Support webauthn (#17957)

Migrate from U2F to Webauthn

Co-authored-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
tokarchuk/v1.17
Lunny Xiao 3 years ago committed by GitHub
parent 8808293247
commit 35c3553870
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .eslintrc
  2. 5
      cmd/serv.go
  3. 5
      custom/conf/app.example.ini
  4. 5
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  5. 7
      go.mod
  6. 20
      go.sum
  7. 2
      models/auth/main_test.go
  8. 154
      models/auth/u2f.go
  9. 100
      models/auth/u2f_test.go
  10. 222
      models/auth/webauthn.go
  11. 69
      models/auth/webauthn_test.go
  12. 7
      models/fixtures/u2f_registration.yml
  13. 8
      models/fixtures/webauthn_credential.yml
  14. 2
      models/migrations/migrations.go
  15. 91
      models/migrations/v207.go
  16. 78
      modules/auth/webauthn/webauthn.go
  17. 26
      modules/auth/webauthn/webauthn_test.go
  18. 2
      modules/generate/generate.go
  19. 11
      modules/setting/setting.go
  20. 40
      options/locale/locale_en-US.ini
  21. 11
      package-lock.json
  22. 1
      package.json
  23. 1
      public/vendor/plugins/u2f/index.js
  24. 14
      routers/web/auth/auth.go
  25. 6
      routers/web/auth/linkaccount.go
  26. 20
      routers/web/auth/oauth.go
  27. 2
      routers/web/auth/oauth_test.go
  28. 136
      routers/web/auth/u2f.go
  29. 169
      routers/web/auth/webauthn.go
  30. 5
      routers/web/user/setting/security/security.go
  31. 111
      routers/web/user/setting/security/u2f.go
  32. 119
      routers/web/user/setting/security/webauthn.go
  33. 21
      routers/web/web.go
  34. 5
      services/auth/auth.go
  35. 2
      services/auth/source/oauth2/jwtsigningkey.go
  36. 4
      services/auth/source/oauth2/token.go
  37. 12
      services/forms/user_form.go
  38. 3
      services/lfs/server.go
  39. 3
      templates/base/footer.tmpl
  40. 24
      templates/user/auth/u2f.tmpl
  41. 32
      templates/user/auth/u2f_error.tmpl
  42. 22
      templates/user/auth/webauthn.tmpl
  43. 22
      templates/user/auth/webauthn_error.tmpl
  44. 2
      templates/user/settings/security/security.tmpl
  45. 23
      templates/user/settings/security/webauthn.tmpl
  46. 24
      vendor/github.com/cloudflare/cfssl/LICENSE
  47. 188
      vendor/github.com/cloudflare/cfssl/crypto/pkcs7/pkcs7.go
  48. 46
      vendor/github.com/cloudflare/cfssl/errors/doc.go
  49. 438
      vendor/github.com/cloudflare/cfssl/errors/error.go
  50. 47
      vendor/github.com/cloudflare/cfssl/errors/http.go
  51. 48
      vendor/github.com/cloudflare/cfssl/helpers/derhelpers/derhelpers.go
  52. 133
      vendor/github.com/cloudflare/cfssl/helpers/derhelpers/ed25519.go
  53. 590
      vendor/github.com/cloudflare/cfssl/helpers/helpers.go
  54. 162
      vendor/github.com/cloudflare/cfssl/log/log.go
  55. 336
      vendor/github.com/cloudflare/cfssl/revoke/revoke.go
  56. 26
      vendor/github.com/duo-labs/webauthn/LICENSE.txt
  57. 617
      vendor/github.com/duo-labs/webauthn/metadata/metadata.go
  58. 155
      vendor/github.com/duo-labs/webauthn/protocol/assertion.go
  59. 156
      vendor/github.com/duo-labs/webauthn/protocol/attestation.go
  60. 227
      vendor/github.com/duo-labs/webauthn/protocol/attestation_androidkey.go
  61. 104
      vendor/github.com/duo-labs/webauthn/protocol/attestation_apple.go
  62. 278
      vendor/github.com/duo-labs/webauthn/protocol/attestation_packed.go
  63. 143
      vendor/github.com/duo-labs/webauthn/protocol/attestation_safetynet.go
  64. 349
      vendor/github.com/duo-labs/webauthn/protocol/attestation_tpm.go
  65. 135
      vendor/github.com/duo-labs/webauthn/protocol/attestation_u2f.go
  66. 256
      vendor/github.com/duo-labs/webauthn/protocol/authenticator.go
  67. 41
      vendor/github.com/duo-labs/webauthn/protocol/base64.go
  68. 27
      vendor/github.com/duo-labs/webauthn/protocol/challenge.go
  69. 112
      vendor/github.com/duo-labs/webauthn/protocol/client.go
  70. 216
      vendor/github.com/duo-labs/webauthn/protocol/credential.go
  71. 8
      vendor/github.com/duo-labs/webauthn/protocol/doc.go
  72. 48
      vendor/github.com/duo-labs/webauthn/protocol/entities.go
  73. 85
      vendor/github.com/duo-labs/webauthn/protocol/errors.go
  74. 8
      vendor/github.com/duo-labs/webauthn/protocol/extensions.go
  75. 282
      vendor/github.com/duo-labs/webauthn/protocol/googletpm/certinfo.go
  76. 152
      vendor/github.com/duo-labs/webauthn/protocol/googletpm/googletpm.go
  77. 240
      vendor/github.com/duo-labs/webauthn/protocol/googletpm/pubarea.go
  78. 136
      vendor/github.com/duo-labs/webauthn/protocol/options.go
  79. 1
      vendor/github.com/duo-labs/webauthn/protocol/signature_algorithms.go
  80. 12
      vendor/github.com/duo-labs/webauthn/protocol/webauthncose/ed25519.go
  81. 38
      vendor/github.com/duo-labs/webauthn/protocol/webauthncose/ed25519_go112.go
  82. 400
      vendor/github.com/duo-labs/webauthn/protocol/webauthncose/webauthncose.go
  83. 51
      vendor/github.com/duo-labs/webauthn/webauthn/authenticator.go
  84. 35
      vendor/github.com/duo-labs/webauthn/webauthn/credential.go
  85. 3
      vendor/github.com/duo-labs/webauthn/webauthn/doc.go
  86. 188
      vendor/github.com/duo-labs/webauthn/webauthn/login.go
  87. 71
      vendor/github.com/duo-labs/webauthn/webauthn/main.go
  88. 170
      vendor/github.com/duo-labs/webauthn/webauthn/registration.go
  89. 13
      vendor/github.com/duo-labs/webauthn/webauthn/session.go
  90. 42
      vendor/github.com/duo-labs/webauthn/webauthn/user.go
  91. 12
      vendor/github.com/fxamacker/cbor/v2/.gitignore
  92. 86
      vendor/github.com/fxamacker/cbor/v2/.golangci.yml
  93. 264
      vendor/github.com/fxamacker/cbor/v2/CBOR_BENCHMARKS.md
  94. 32
      vendor/github.com/fxamacker/cbor/v2/CBOR_GOLANG.md
  95. 76
      vendor/github.com/fxamacker/cbor/v2/CODE_OF_CONDUCT.md
  96. 47
      vendor/github.com/fxamacker/cbor/v2/CONTRIBUTING.md
  97. 21
      vendor/github.com/fxamacker/cbor/v2/LICENSE
  98. 938
      vendor/github.com/fxamacker/cbor/v2/README.md
  99. 308
      vendor/github.com/fxamacker/cbor/v2/cache.go
  100. 1642
      vendor/github.com/fxamacker/cbor/v2/decode.go
  101. Some files were not shown because too many files have changed in this diff Show More

@ -27,7 +27,6 @@ globals:
CodeMirror: false CodeMirror: false
Dropzone: false Dropzone: false
SimpleMDE: false SimpleMDE: false
u2fApi: false
settings: settings:
html/html-extensions: [".tmpl"] html/html-extensions: [".tmpl"]

@ -27,7 +27,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/lfs" "code.gitea.io/gitea/services/lfs"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -253,7 +253,8 @@ func runServ(c *cli.Context) error {
now := time.Now() now := time.Now()
claims := lfs.Claims{ claims := lfs.Claims{
StandardClaims: jwt.StandardClaims{ // FIXME: we need to migrate to RegisteredClaims
StandardClaims: jwt.StandardClaims{ // nolint
ExpiresAt: now.Add(setting.LFS.HTTPAuthExpiry).Unix(), ExpiresAt: now.Add(setting.LFS.HTTPAuthExpiry).Unix(),
NotBefore: now.Unix(), NotBefore: now.Unix(),
}, },

@ -435,9 +435,10 @@ ENABLE = true
;; NOTE: THE DEFAULT VALUES HERE WILL NEED TO BE CHANGED ;; NOTE: THE DEFAULT VALUES HERE WILL NEED TO BE CHANGED
;; Two Factor authentication with security keys ;; Two Factor authentication with security keys
;; https://developers.yubico.com/U2F/App_ID.html ;; https://developers.yubico.com/U2F/App_ID.html
;;
;; DEPRECATED - this only applies to previously registered security keys using the U2F standard
APP_ID = ; e.g. http://localhost:3000/ APP_ID = ; e.g. http://localhost:3000/
;; Comma separated list of trusted facets
TRUSTED_FACETS = ; e.g. http://localhost:3000/
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

@ -974,9 +974,8 @@ NB: You must have `DISABLE_ROUTER_LOG` set to `false` for this option to take ef
- `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN**: List of locales shown in language selector - `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN**: List of locales shown in language selector
- `NAMES`: **English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയ**: Visible names corresponding to the locales - `NAMES`: **English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയ**: Visible names corresponding to the locales
## U2F (`U2F`) ## U2F (`U2F`) **DEPRECATED**
- `APP_ID`: **`ROOT_URL`**: Declares the facet of the application. Requires HTTPS. - `APP_ID`: **`ROOT_URL`**: Declares the facet of the application which is used for authentication of previously registered U2F keys. Requires HTTPS.
- `TRUSTED_FACETS`: List of additional facets which are trusted. This is not support by all browsers.
## Markup (`markup`) ## Markup (`markup`)

@ -30,6 +30,7 @@ require (
github.com/denisenkom/go-mssqldb v0.10.0 github.com/denisenkom/go-mssqldb v0.10.0
github.com/djherbis/buffer v1.2.0 github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1 github.com/djherbis/nio/v3 v3.0.1
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/editorconfig/editorconfig-core-go/v2 v2.4.2 github.com/editorconfig/editorconfig-core-go/v2 v2.4.2
github.com/emirpasic/gods v1.12.0 github.com/emirpasic/gods v1.12.0
@ -50,7 +51,7 @@ require (
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v4 v4.2.0
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-github/v39 v39.2.0 github.com/google/go-github/v39 v39.2.0
github.com/google/uuid v1.2.0 github.com/google/uuid v1.2.0
@ -140,6 +141,8 @@ require (
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/golang-jwt/jwt v3.2.1+incompatible => github.com/golang-jwt/jwt v3.2.2+incompatible replace github.com/markbates/goth v1.68.0 => github.com/zeripath/goth v1.68.1-0.20220109111530-754359885dce
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/duo-labs/webauthn => github.com/authelia/webauthn v0.0.0-20211225121951-80d1f2a572e4

@ -131,6 +131,8 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:o
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/authelia/webauthn v0.0.0-20211225121951-80d1f2a572e4 h1:u3eFvgr4A8IjlAokbFt6XY6VdurX7DEYnQMQ4K2yobc=
github.com/authelia/webauthn v0.0.0-20211225121951-80d1f2a572e4/go.mod h1:EYSpSkwoEcryMmQGfhol2IiB3IMN9IIIaNd/wcAQMGQ=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
@ -209,6 +211,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1CUzYdAZXijuvLuRMirgiXdf3zsM2Ig=
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@ -303,6 +307,8 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ=
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA= github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
@ -498,8 +504,9 @@ github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 h1:yXtpJr/LV6PFu4nTLgfjQ
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3G5amEoiNbhVrmM+ZQEcnQvNQ2KaZk= github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3G5amEoiNbhVrmM+ZQEcnQvNQ2KaZk=
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -540,6 +547,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -819,8 +828,6 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0= github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.68.0 h1:90sKvjRAKHcl9V2uC9x/PJXeD78cFPiBsyP1xVhoQfA=
github.com/markbates/goth v1.68.0/go.mod h1:V2VcDMzDiMHW+YmqYl7i0cMiAUeCkAe4QE6jRKBhXZw=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
@ -1034,6 +1041,7 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@ -1141,6 +1149,8 @@ github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ= github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ=
github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE= github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
@ -1169,6 +1179,8 @@ github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod
github.com/yuin/goldmark-meta v1.0.0 h1:ScsatUIT2gFS6azqzLGUjgOnELsBOxMXerM3ogdJhAM= github.com/yuin/goldmark-meta v1.0.0 h1:ScsatUIT2gFS6azqzLGUjgOnELsBOxMXerM3ogdJhAM=
github.com/yuin/goldmark-meta v1.0.0/go.mod h1:zsNNOrZ4nLuyHAJeLQEZcQat8dm70SmB2kHbls092Gc= github.com/yuin/goldmark-meta v1.0.0/go.mod h1:zsNNOrZ4nLuyHAJeLQEZcQat8dm70SmB2kHbls092Gc=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zeripath/goth v1.68.1-0.20220109111530-754359885dce h1:ul/k+Fu3/2h+hxIaEMrn6m96X1Wf+TQk9G7zyuvy1Ws=
github.com/zeripath/goth v1.68.1-0.20220109111530-754359885dce/go.mod h1:uk3KIdtCKdmyNABgOSmHFNHN0AcKqkLs8j5Ak3Ioe1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=

@ -17,6 +17,6 @@ func TestMain(m *testing.M) {
"oauth2_application.yml", "oauth2_application.yml",
"oauth2_authorization_code.yml", "oauth2_authorization_code.yml",
"oauth2_grant.yml", "oauth2_grant.yml",
"u2f_registration.yml", "webauthn_credential.yml",
) )
} }

@ -1,154 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"github.com/tstranex/u2f"
)
// ____ ________________________________ .__ __ __ .__
// | | \_____ \_ _____/\______ \ ____ ____ |__| _______/ |_____________ _/ |_|__| ____ ____
// | | // ____/| __) | _// __ \ / ___\| |/ ___/\ __\_ __ \__ \\ __\ |/ _ \ / \
// | | // \| \ | | \ ___// /_/ > |\___ \ | | | | \// __ \| | | ( <_> ) | \
// |______/ \_______ \___ / |____|_ /\___ >___ /|__/____ > |__| |__| (____ /__| |__|\____/|___| /
// \/ \/ \/ \/_____/ \/ \/ \/
// ErrU2FRegistrationNotExist represents a "ErrU2FRegistrationNotExist" kind of error.
type ErrU2FRegistrationNotExist struct {
ID int64
}
func (err ErrU2FRegistrationNotExist) Error() string {
return fmt.Sprintf("U2F registration does not exist [id: %d]", err.ID)
}
// IsErrU2FRegistrationNotExist checks if an error is a ErrU2FRegistrationNotExist.
func IsErrU2FRegistrationNotExist(err error) bool {
_, ok := err.(ErrU2FRegistrationNotExist)
return ok
}
// U2FRegistration represents the registration data and counter of a security key
type U2FRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
db.RegisterModel(new(U2FRegistration))
}
// TableName returns a better table name for U2FRegistration
func (reg U2FRegistration) TableName() string {
return "u2f_registration"
}
// Parse will convert the db entry U2FRegistration to an u2f.Registration struct
func (reg *U2FRegistration) Parse() (*u2f.Registration, error) {
r := new(u2f.Registration)
return r, r.UnmarshalBinary(reg.Raw)
}
func (reg *U2FRegistration) updateCounter(e db.Engine) error {
_, err := e.ID(reg.ID).Cols("counter").Update(reg)
return err
}
// UpdateCounter will update the database value of counter
func (reg *U2FRegistration) UpdateCounter() error {
return reg.updateCounter(db.GetEngine(db.DefaultContext))
}
// U2FRegistrationList is a list of *U2FRegistration
type U2FRegistrationList []*U2FRegistration
// ToRegistrations will convert all U2FRegistrations to u2f.Registrations
func (list U2FRegistrationList) ToRegistrations() []u2f.Registration {
regs := make([]u2f.Registration, 0, len(list))
for _, reg := range list {
r, err := reg.Parse()
if err != nil {
log.Error("parsing u2f registration: %v", err)
continue
}
regs = append(regs, *r)
}
return regs
}
func getU2FRegistrationsByUID(e db.Engine, uid int64) (U2FRegistrationList, error) {
regs := make(U2FRegistrationList, 0)
return regs, e.Where("user_id = ?", uid).Find(&regs)
}
// GetU2FRegistrationByID returns U2F registration by id
func GetU2FRegistrationByID(id int64) (*U2FRegistration, error) {
return getU2FRegistrationByID(db.GetEngine(db.DefaultContext), id)
}
func getU2FRegistrationByID(e db.Engine, id int64) (*U2FRegistration, error) {
reg := new(U2FRegistration)
if found, err := e.ID(id).Get(reg); err != nil {
return nil, err
} else if !found {
return nil, ErrU2FRegistrationNotExist{ID: id}
}
return reg, nil
}
// GetU2FRegistrationsByUID returns all U2F registrations of the given user
func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) {
return getU2FRegistrationsByUID(db.GetEngine(db.DefaultContext), uid)
}
// HasU2FRegistrationsByUID returns whether a given user has U2F registrations
func HasU2FRegistrationsByUID(uid int64) (bool, error) {
return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&U2FRegistration{})
}
func createRegistration(e db.Engine, userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) {
raw, err := reg.MarshalBinary()
if err != nil {
return nil, err
}
r := &U2FRegistration{
UserID: userID,
Name: name,
Counter: 0,
Raw: raw,
}
_, err = e.InsertOne(r)
if err != nil {
return nil, err
}
return r, nil
}
// CreateRegistration will create a new U2FRegistration from the given Registration
func CreateRegistration(userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) {
return createRegistration(db.GetEngine(db.DefaultContext), userID, name, reg)
}
// DeleteRegistration will delete U2FRegistration
func DeleteRegistration(reg *U2FRegistration) error {
return deleteRegistration(db.GetEngine(db.DefaultContext), reg)
}
func deleteRegistration(e db.Engine, reg *U2FRegistration) error {
_, err := e.Delete(reg)
return err
}

@ -1,100 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"encoding/hex"
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/tstranex/u2f"
)
func TestGetU2FRegistrationByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := GetU2FRegistrationByID(1)
assert.NoError(t, err)
assert.Equal(t, "U2F Key", res.Name)
_, err = GetU2FRegistrationByID(342432)
assert.Error(t, err)
assert.True(t, IsErrU2FRegistrationNotExist(err))
}
func TestGetU2FRegistrationsByUID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := GetU2FRegistrationsByUID(32)
assert.NoError(t, err)
assert.Len(t, res, 1)
assert.Equal(t, "U2F Key", res[0].Name)
}
func TestU2FRegistration_TableName(t *testing.T) {
assert.Equal(t, "u2f_registration", U2FRegistration{}.TableName())
}
func TestU2FRegistration_UpdateCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
reg.Counter = 1
assert.NoError(t, reg.UpdateCounter())
unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1})
}
func TestU2FRegistration_UpdateLargeCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
reg.Counter = 0xffffffff
assert.NoError(t, reg.UpdateCounter())
unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 0xffffffff})
}
func TestCreateRegistration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := CreateRegistration(1, "U2F Created Key", &u2f.Registration{Raw: []byte("Test")})
assert.NoError(t, err)
assert.Equal(t, "U2F Created Key", res.Name)
assert.Equal(t, []byte("Test"), res.Raw)
unittest.AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: 1})
}
func TestDeleteRegistration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
assert.NoError(t, DeleteRegistration(reg))
unittest.AssertNotExistsBean(t, &U2FRegistration{ID: 1})
}
const validU2FRegistrationResponseHex = "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871"
func TestToRegistrations_SkipInvalidItemsWithoutCrashing(t *testing.T) {
regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex)
regs := U2FRegistrationList{
&U2FRegistration{ID: 1},
&U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800},
}
actual := regs.ToRegistrations()
assert.Len(t, actual, 1)
}
func TestToRegistrations(t *testing.T) {
regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex)
regs := U2FRegistrationList{
&U2FRegistration{ID: 1, Name: "U2F Key", UserID: 1, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800},
&U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800},
}
actual := regs.ToRegistrations()
assert.Len(t, actual, 2)
}

@ -0,0 +1,222 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"context"
"encoding/base64"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
"github.com/duo-labs/webauthn/webauthn"
)
// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
type ErrWebAuthnCredentialNotExist struct {
ID int64
CredentialID string
}
func (err ErrWebAuthnCredentialNotExist) Error() string {
if err.CredentialID == "" {
return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID)
}
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %s]", err.CredentialID)
}
//IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist.
func IsErrWebAuthnCredentialNotExist(err error) bool {
_, ok := err.(ErrWebAuthnCredentialNotExist)
return ok
}
//WebAuthnCredential represents the WebAuthn credential data for a public-key
//credential conformant to WebAuthn Level 1
type WebAuthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX"`
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
db.RegisterModel(new(WebAuthnCredential))
}
// TableName returns a better table name for WebAuthnCredential
func (cred WebAuthnCredential) TableName() string {
return "webauthn_credential"
}
// UpdateSignCount will update the database value of SignCount
func (cred *WebAuthnCredential) UpdateSignCount() error {
return cred.updateSignCount(db.DefaultContext)
}
func (cred *WebAuthnCredential) updateSignCount(ctx context.Context) error {
_, err := db.GetEngine(ctx).ID(cred.ID).Cols("sign_count").Update(cred)
return err
}
// BeforeInsert will be invoked by XORM before updating a record
func (cred *WebAuthnCredential) BeforeInsert() {
cred.LowerName = strings.ToLower(cred.Name)
}
// BeforeUpdate will be invoked by XORM before updating a record
func (cred *WebAuthnCredential) BeforeUpdate() {
cred.LowerName = strings.ToLower(cred.Name)
}
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (cred *WebAuthnCredential) AfterLoad(session *xorm.Session) {
cred.LowerName = strings.ToLower(cred.Name)
}
// WebAuthnCredentialList is a list of *WebAuthnCredential
type WebAuthnCredentialList []*WebAuthnCredential
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
creds := make([]webauthn.Credential, 0, len(list))
for _, cred := range list {
credID, _ := base64.RawStdEncoding.DecodeString(cred.CredentialID)
creds = append(creds, webauthn.Credential{
ID: credID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID,
SignCount: cred.SignCount,
CloneWarning: cred.CloneWarning,
},
})
}
return creds
}
//GetWebAuthnCredentialsByUID returns all WebAuthn credentials of the given user
func GetWebAuthnCredentialsByUID(uid int64) (WebAuthnCredentialList, error) {
return getWebAuthnCredentialsByUID(db.DefaultContext, uid)
}
func getWebAuthnCredentialsByUID(ctx context.Context, uid int64) (WebAuthnCredentialList, error) {
creds := make(WebAuthnCredentialList, 0)
return creds, db.GetEngine(ctx).Where("user_id = ?", uid).Find(&creds)
}
//ExistsWebAuthnCredentialsForUID returns if the given user has credentials
func ExistsWebAuthnCredentialsForUID(uid int64) (bool, error) {
return existsWebAuthnCredentialsByUID(db.DefaultContext, uid)
}
func existsWebAuthnCredentialsByUID(ctx context.Context, uid int64) (bool, error) {
return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{})
}
// GetWebAuthnCredentialByName returns WebAuthn credential by id
func GetWebAuthnCredentialByName(uid int64, name string) (*WebAuthnCredential, error) {
return getWebAuthnCredentialByName(db.DefaultContext, uid, name)
}
func getWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*WebAuthnCredential, error) {
cred := new(WebAuthnCredential)
if found, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ?", uid, strings.ToLower(name)).Get(cred); err != nil {
return nil, err
} else if !found {
return nil, ErrWebAuthnCredentialNotExist{}
}
return cred, nil
}
// GetWebAuthnCredentialByID returns WebAuthn credential by id
func GetWebAuthnCredentialByID(id int64) (*WebAuthnCredential, error) {
return getWebAuthnCredentialByID(db.DefaultContext, id)
}
func getWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) {
cred := new(WebAuthnCredential)
if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil {
return nil, err
} else if !found {
return nil, ErrWebAuthnCredentialNotExist{ID: id}
}
return cred, nil
}
// HasWebAuthnRegistrationsByUID returns whether a given user has WebAuthn registrations
func HasWebAuthnRegistrationsByUID(uid int64) (bool, error) {
return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&WebAuthnCredential{})
}
// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
func GetWebAuthnCredentialByCredID(credID string) (*WebAuthnCredential, error) {
return getWebAuthnCredentialByCredID(db.DefaultContext, credID)
}
func getWebAuthnCredentialByCredID(ctx context.Context, credID string) (*WebAuthnCredential, error) {
cred := new(WebAuthnCredential)
if found, err := db.GetEngine(ctx).Where("credential_id = ?", credID).Get(cred); err != nil {
return nil, err
} else if !found {
return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID}
}
return cred, nil
}
// CreateCredential will create a new WebAuthnCredential from the given Credential
func CreateCredential(userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
return createCredential(db.DefaultContext, userID, name, cred)
}
func createCredential(ctx context.Context, userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
c := &WebAuthnCredential{
UserID: userID,
Name: name,
CredentialID: base64.RawStdEncoding.EncodeToString(cred.ID),
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
AAGUID: cred.Authenticator.AAGUID,
SignCount: cred.Authenticator.SignCount,
CloneWarning: false,
}
if err := db.Insert(ctx, c); err != nil {
return nil, err
}
return c, nil
}
// DeleteCredential will delete WebAuthnCredential
func DeleteCredential(id, userID int64) (bool, error) {
return deleteCredential(db.DefaultContext, id, userID)
}
func deleteCredential(ctx context.Context, id, userID int64) (bool, error) {
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
return had > 0, err
}
//WebAuthnCredentials implementns the webauthn.User interface
func WebAuthnCredentials(userID int64) ([]webauthn.Credential, error) {
dbCreds, err := GetWebAuthnCredentialsByUID(userID)
if err != nil {
return nil, err
}
return dbCreds.ToCredentials(), nil
}

@ -0,0 +1,69 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"encoding/base64"
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/duo-labs/webauthn/webauthn"
"github.com/stretchr/testify/assert"
)
func TestGetWebAuthnCredentialByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := GetWebAuthnCredentialByID(1)
assert.NoError(t, err)
assert.Equal(t, "WebAuthn credential", res.Name)
_, err = GetWebAuthnCredentialByID(342432)
assert.Error(t, err)
assert.True(t, IsErrWebAuthnCredentialNotExist(err))
}
func TestGetWebAuthnCredentialsByUID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := GetWebAuthnCredentialsByUID(32)
assert.NoError(t, err)
assert.Len(t, res, 1)
assert.Equal(t, "WebAuthn credential", res[0].Name)
}
func TestWebAuthnCredential_TableName(t *testing.T) {
assert.Equal(t, "webauthn_credential", WebAuthnCredential{}.TableName())
}
func TestWebAuthnCredential_UpdateSignCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential)
cred.SignCount = 1
assert.NoError(t, cred.UpdateSignCount())
unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 1})
}
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential)
cred.SignCount = 0xffffffff
assert.NoError(t, cred.UpdateSignCount())
unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
}
func TestCreateCredential(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
assert.NoError(t, err)
assert.Equal(t, "WebAuthn Created Credential", res.Name)
bs, err := base64.RawStdEncoding.DecodeString(res.CredentialID)
assert.NoError(t, err)
assert.Equal(t, []byte("Test"), bs)
unittest.AssertExistsIf(t, true, &WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
}

@ -1,7 +0,0 @@
-
id: 1
name: "U2F Key"
user_id: 32
counter: 0
created_unix: 946684800
updated_unix: 946684800

@ -0,0 +1,8 @@
- id: 1
name: "WebAuthn credential"
user_id: 32
attestation_type: none
sign_count: 0
clone_warning: false
created_unix: 946684800
updated_unix: 946684800

@ -366,6 +366,8 @@ var migrations = []Migration{
NewMigration("Migrate to higher varchar on user struct", migrateUserPasswordSalt), NewMigration("Migrate to higher varchar on user struct", migrateUserPasswordSalt),
// v206 -> v207 // v206 -> v207
NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit), NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit),
// v207 -> v208
NewMigration("Add webauthn table and migrate u2f data to webauthn", addWebAuthnCred),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

@ -0,0 +1,91 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"crypto/elliptic"
"encoding/base64"
"strings"
"code.gitea.io/gitea/modules/timeutil"
"github.com/tstranex/u2f"
"xorm.io/xorm"
)
func addWebAuthnCred(x *xorm.Engine) error {
// Create webauthnCredential table
type webauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX"`
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
if err := x.Sync2(&webauthnCredential{}); err != nil {
return err
}
// Now migrate the old u2f registrations to the new format
type u2fRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
var start int
regs := make([]*u2fRegistration, 0, 50)
for {
err := x.OrderBy("id").Limit(50, start).Find(&regs)
if err != nil {
return err
}
for _, reg := range regs {
parsed := new(u2f.Registration)
err = parsed.UnmarshalBinary(reg.Raw)
if err != nil {
continue
}
c := &webauthnCredential{
ID: reg.ID,
Name: reg.Name,
LowerName: strings.ToLower(reg.Name),
UserID: reg.UserID,
CredentialID: base64.RawStdEncoding.EncodeToString(parsed.KeyHandle),
PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y),
AttestationType: "fido-u2f",
AAGUID: []byte{},
SignCount: reg.Counter,
}
_, err := x.Insert(c)
if err != nil {
return err
}
}
if len(regs) < 50 {
break
}
start += 50
regs = regs[:0]
}
return nil
}

@ -0,0 +1,78 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package webauthn
import (
"encoding/binary"
"encoding/gob"
"net/url"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
//WebAuthn represents the global WebAuthn instance
var WebAuthn *webauthn.WebAuthn
//Init initializes the WebAuthn instance from the config.
func Init() {
gob.Register(&webauthn.SessionData{})
appURL, _ := url.Parse(setting.AppURL)
WebAuthn = &webauthn.WebAuthn{
Config: &webauthn.Config{
RPDisplayName: setting.AppName,
RPID: setting.Domain,
RPOrigin: protocol.FullyQualifiedOrigin(appURL),
AuthenticatorSelection: protocol.AuthenticatorSelection{
UserVerification: "discouraged",
},
AttestationPreference: protocol.PreferDirectAttestation,
},
}
}
// User represents an implementation of webauthn.User based on User model
type User user_model.User
//WebAuthnID implements the webauthn.User interface
func (u *User) WebAuthnID() []byte {
id := make([]byte, 8)
binary.PutVarint(id, u.ID)
return id
}
//WebAuthnName implements the webauthn.User interface
func (u *User) WebAuthnName() string {
if u.LoginName == "" {
return u.Name
}
return u.LoginName
}
//WebAuthnDisplayName implements the webauthn.User interface
func (u *User) WebAuthnDisplayName() string {
return (*user_model.User)(u).DisplayName()
}
//WebAuthnIcon implements the webauthn.User interface
func (u *User) WebAuthnIcon() string {
return (*user_model.User)(u).AvatarLink()
}
//WebAuthnCredentials implementns the webauthn.User interface
func (u *User) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ID)
if err != nil {
return nil
}
return dbCreds.ToCredentials()
}

@ -0,0 +1,26 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package webauthn
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
setting.Domain = "domain"
setting.AppName = "AppName"
setting.AppURL = "https://domain/"
rpOrigin := "https://domain"
Init()
assert.Equal(t, setting.Domain, WebAuthn.Config.RPID)
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName)
assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigin)
}

@ -13,7 +13,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
) )
// NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN. // NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN.

@ -28,7 +28,6 @@ import (
"code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/user"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
shellquote "github.com/kballard/go-shellquote"
"github.com/unknwon/com" "github.com/unknwon/com"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
ini "gopkg.in/ini.v1" ini "gopkg.in/ini.v1"
@ -388,8 +387,7 @@ var (
} }
U2F = struct { U2F = struct {
AppID string AppID string
TrustedFacets []string
}{} }{}
// Metrics settings // Metrics settings
@ -1015,10 +1013,6 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
newMarkup() newMarkup()
sec = Cfg.Section("U2F")
U2F.TrustedFacets, _ = shellquote.Split(sec.Key("TRUSTED_FACETS").MustString(strings.TrimSuffix(AppURL, AppSubURL+"/")))
U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimSuffix(AppURL, "/"))
UI.ReactionsMap = make(map[string]bool) UI.ReactionsMap = make(map[string]bool)
for _, reaction := range UI.Reactions { for _, reaction := range UI.Reactions {
UI.ReactionsMap[reaction] = true UI.ReactionsMap[reaction] = true
@ -1027,6 +1021,9 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
for _, emoji := range UI.CustomEmojis { for _, emoji := range UI.CustomEmojis {
UI.CustomEmojisMap[emoji] = ":" + emoji + ":" UI.CustomEmojisMap[emoji] = ":" + emoji + ":"
} }
sec = Cfg.Section("U2F")
U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimSuffix(AppURL, "/"))
} }
func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) { func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {

@ -34,18 +34,20 @@ twofa = Two-Factor Authentication
twofa_scratch = Two-Factor Scratch Code twofa_scratch = Two-Factor Scratch Code
passcode = Passcode passcode = Passcode
u2f_insert_key = Insert your security key webauthn_insert_key = Insert your security key
u2f_sign_in = Press the button on your security key. If your security key has no button, re-insert it. webauthn_sign_in = Press the button on your security key. If your security key has no button, re-insert it.
u2f_press_button = Please press the button on your security key… webauthn_press_button = Please press the button on your security key…
u2f_use_twofa = Use a two-factor code from your phone webauthn_use_twofa = Use a two-factor code from your phone
u2f_error = Could not read your security key. webauthn_error = Could not read your security key.
u2f_unsupported_browser = Your browser does not support U2F security keys. webauthn_unsupported_browser = Your browser does not currently support WebAuthn.
u2f_error_1 = An unknown error occurred. Please retry. webauthn_error_unknown = An unknown error occurred. Please retry.
u2f_error_2 = Please make sure to use the correct, encrypted (https://) URL. webauthn_error_insecure = WebAuthn only supports secure connections. For testing over HTTP, you can use the origin "localhost" or "127.0.0.1"
u2f_error_3 = The server could not process your request. webauthn_error_unable_to_process = The server could not process your request.
u2f_error_4 = The security key is not permitted for this request. Please make sure that the key is not already registered. webauthn_error_duplicated = The security key is not permitted for this request. Please make sure that the key is not already registered.
u2f_error_5 = Timeout reached before your key could be read. Please reload this page and retry. webauthn_error_empty = You must set a name for this key.
u2f_reload = Reload webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry.
webauthn_u2f_deprecated = The key: '%s' authenticates using the deprecated U2F process. You should re-register this key and remove the old registration.
webauthn_reload = Reload
repository = Repository repository = Repository
organization = Organization organization = Organization
@ -525,7 +527,7 @@ twofa = Two-Factor Authentication
account_link = Linked Accounts account_link = Linked Accounts
organization = Organizations organization = Organizations
uid = Uid uid = Uid
u2f = Security Keys webauthn = Security Keys
public_profile = Public Profile public_profile = Public Profile
biography_placeholder = Tell us a little bit about yourself biography_placeholder = Tell us a little bit about yourself
@ -746,12 +748,12 @@ passcode_invalid = The passcode is incorrect. Try again.
twofa_enrolled = Your account has been enrolled into two-factor authentication. Store your scratch token (%s) in a safe place as it is only shown once! twofa_enrolled = Your account has been enrolled into two-factor authentication. Store your scratch token (%s) in a safe place as it is only shown once!
twofa_failed_get_secret = Failed to get secret. twofa_failed_get_secret = Failed to get secret.
u2f_desc = Security keys are hardware devices containing cryptographic keys. They can be used for two-factor authentication. Security keys must support the <a rel="noreferrer" href="https://fidoalliance.org/">FIDO U2F</a> standard. webauthn_desc = Security keys are hardware devices containing cryptographic keys. They can be used for two-factor authentication. Security keys must support the <a rel="noreferrer" href="https://w3c.github.io/webauthn/#webauthn-authenticator">WebAuthn Authenticator</a> standard.
u2f_register_key = Add Security Key webauthn_register_key = Add Security Key
u2f_nickname = Nickname webauthn_nickname = Nickname
u2f_press_button = Press the button on your security key to register it. webauthn_press_button = Press the button on your security key to register it.
u2f_delete_key = Remove Security Key webauthn_delete_key = Remove Security Key
u2f_delete_key_desc = If you remove a security key you can no longer sign in with it. Continue? webauthn_delete_key_desc = If you remove a security key you can no longer sign in with it. Continue?
manage_account_links = Manage Linked Accounts manage_account_links = Manage Linked Accounts
manage_account_links_desc = These external accounts are linked to your Gitea account. manage_account_links_desc = These external accounts are linked to your Gitea account.

11
package-lock.json generated

@ -30,6 +30,7 @@
"sortablejs": "1.14.0", "sortablejs": "1.14.0",
"swagger-ui-dist": "4.1.3", "swagger-ui-dist": "4.1.3",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
"vue": "2.6.14", "vue": "2.6.14",
"vue-bar-graph": "1.3.0", "vue-bar-graph": "1.3.0",
"vue-calendar-heatmap": "0.8.4", "vue-calendar-heatmap": "0.8.4",
@ -9738,6 +9739,11 @@
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.1.tgz",
"integrity": "sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==" "integrity": "sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg=="
}, },
"node_modules/uint8-to-base64": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz",
"integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA=="
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@ -17882,6 +17888,11 @@
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.1.tgz",
"integrity": "sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==" "integrity": "sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg=="
}, },
"uint8-to-base64": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz",
"integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA=="
},
"unbox-primitive": { "unbox-primitive": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",

@ -30,6 +30,7 @@
"sortablejs": "1.14.0", "sortablejs": "1.14.0",
"swagger-ui-dist": "4.1.3", "swagger-ui-dist": "4.1.3",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
"vue": "2.6.14", "vue": "2.6.14",
"vue-bar-graph": "1.3.0", "vue-bar-graph": "1.3.0",
"vue-calendar-heatmap": "0.8.4", "vue-calendar-heatmap": "0.8.4",

File diff suppressed because one or more lines are too long

@ -236,14 +236,14 @@ func SignInPost(ctx *context.Context) {
return return
} }
// Check if the user has u2f registration // Check if the user has webauthn registration
hasU2Ftwofa, err := auth.HasU2FRegistrationsByUID(u.ID) hasWebAuthnTwofa, err := auth.HasWebAuthnRegistrationsByUID(u.ID)
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
} }
if !hasTOTPtwofa && !hasU2Ftwofa { if !hasTOTPtwofa && !hasWebAuthnTwofa {
// No two factor auth configured we can sign in the user // No two factor auth configured we can sign in the user
handleSignIn(ctx, u, form.Remember) handleSignIn(ctx, u, form.Remember)
return return
@ -254,7 +254,7 @@ func SignInPost(ctx *context.Context) {
return return
} }
// User will need to use 2FA TOTP or U2F, save data // User will need to use 2FA TOTP or WebAuthn, save data
if err := ctx.Session.Set("twofaUid", u.ID); err != nil { if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err) ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
return return
@ -268,7 +268,7 @@ func SignInPost(ctx *context.Context) {
if hasTOTPtwofa { if hasTOTPtwofa {
// User will need to use U2F, save data // User will need to use U2F, save data
if err := ctx.Session.Set("totpEnrolled", u.ID); err != nil { if err := ctx.Session.Set("totpEnrolled", u.ID); err != nil {
ctx.ServerError("UserSignIn: Unable to set u2fEnrolled in session", err) ctx.ServerError("UserSignIn: Unable to set WebAuthn Enrolled in session", err)
return return
} }
} }
@ -279,8 +279,8 @@ func SignInPost(ctx *context.Context) {
} }
// If we have U2F redirect there first // If we have U2F redirect there first
if hasU2Ftwofa { if hasWebAuthnTwofa {
ctx.Redirect(setting.AppSubURL + "/user/u2f") ctx.Redirect(setting.AppSubURL + "/user/webauthn")
return return
} }

@ -172,10 +172,10 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r
log.Error("Error storing session: %v", err) log.Error("Error storing session: %v", err)
} }
// If U2F is enrolled -> Redirect to U2F instead // If WebAuthn is enrolled -> Redirect to WebAuthn instead
regs, err := auth.GetU2FRegistrationsByUID(u.ID) regs, err := auth.GetWebAuthnCredentialsByUID(u.ID)
if err == nil && len(regs) > 0 { if err == nil && len(regs) > 0 {
ctx.Redirect(setting.AppSubURL + "/user/u2f") ctx.Redirect(setting.AppSubURL + "/user/webauthn")
return return
} }

@ -34,7 +34,7 @@ import (
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
"github.com/markbates/goth" "github.com/markbates/goth"
) )
@ -149,7 +149,8 @@ func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2
accessToken := &oauth2.Token{ accessToken := &oauth2.Token{
GrantID: grant.ID, GrantID: grant.ID,
Type: oauth2.TypeAccessToken, Type: oauth2.TypeAccessToken,
StandardClaims: jwt.StandardClaims{ // FIXME: Migrate to RegisteredClaims
StandardClaims: jwt.StandardClaims{ //nolint
ExpiresAt: expirationDate.AsTime().Unix(), ExpiresAt: expirationDate.AsTime().Unix(),
}, },
} }
@ -167,7 +168,8 @@ func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2
GrantID: grant.ID, GrantID: grant.ID,
Counter: grant.Counter, Counter: grant.Counter,
Type: oauth2.TypeRefreshToken, Type: oauth2.TypeRefreshToken,
StandardClaims: jwt.StandardClaims{ // FIXME: Migrate to RegisteredClaims
StandardClaims: jwt.StandardClaims{ // nolint
ExpiresAt: refreshExpirationDate, ExpiresAt: refreshExpirationDate,
}, },
} }
@ -205,7 +207,8 @@ func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2
} }
idToken := &oauth2.OIDCToken{ idToken := &oauth2.OIDCToken{
StandardClaims: jwt.StandardClaims{ // FIXME: migrate to RegisteredClaims
StandardClaims: jwt.StandardClaims{ //nolint
ExpiresAt: expirationDate.AsTime().Unix(), ExpiresAt: expirationDate.AsTime().Unix(),
Issuer: setting.AppURL, Issuer: setting.AppURL,
Audience: app.ClientID, Audience: app.ClientID,
@ -326,7 +329,8 @@ func IntrospectOAuth(ctx *context.Context) {
var response struct { var response struct {
Active bool `json:"active"` Active bool `json:"active"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
jwt.StandardClaims // FIXME: Migrate to RegisteredClaims
jwt.StandardClaims //nolint
} }
form := web.GetForm(ctx).(*forms.IntrospectTokenForm) form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
@ -1066,10 +1070,10 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
log.Error("Error storing session: %v", err) log.Error("Error storing session: %v", err)
} }
// If U2F is enrolled -> Redirect to U2F instead // If WebAuthn is enrolled -> Redirect to WebAuthn instead
regs, err := auth.GetU2FRegistrationsByUID(u.ID) regs, err := auth.GetWebAuthnCredentialsByUID(u.ID)
if err == nil && len(regs) > 0 { if err == nil && len(regs) > 0 {
ctx.Redirect(setting.AppSubURL + "/user/u2f") ctx.Redirect(setting.AppSubURL + "/user/webauthn")
return return
} }

@ -12,7 +12,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

@ -1,136 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/externalaccount"
"github.com/tstranex/u2f"
)
var tplU2F base.TplName = "user/auth/u2f"
// U2F shows the U2F login page
func U2F(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
ctx.Data["RequireU2F"] = true
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
// Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
return
}
// See whether TOTP is also available.
if ctx.Session.Get("totpEnrolled") != nil {
ctx.Data["TOTPEnrolled"] = true
}
ctx.HTML(http.StatusOK, tplU2F)
}
// U2FChallenge submits a sign challenge to the browser
func U2FChallenge(ctx *context.Context) {
// Ensure user is in a U2F session.
idSess := ctx.Session.Get("twofaUid")
if idSess == nil {
ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
return
}
id := idSess.(int64)
regs, err := auth.GetU2FRegistrationsByUID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if len(regs) == 0 {
ctx.ServerError("UserSignIn", errors.New("no device registered"))
return
}
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
if err != nil {
ctx.ServerError("u2f.NewChallenge", err)
return
}
if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err)
return
}
if err := ctx.Session.Release(); err != nil {
ctx.ServerError("UserSignIn: unable to store session", err)
}
ctx.JSON(http.StatusOK, challenge.SignRequest(regs.ToRegistrations()))
}
// U2FSign authenticates the user by signResp
func U2FSign(ctx *context.Context) {
signResp := web.GetForm(ctx).(*u2f.SignResponse)
challSess := ctx.Session.Get("u2fChallenge")
idSess := ctx.Session.Get("twofaUid")
if challSess == nil || idSess == nil {
ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
return
}
challenge := challSess.(*u2f.Challenge)
id := idSess.(int64)
regs, err := auth.GetU2FRegistrationsByUID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
for _, reg := range regs {
r, err := reg.Parse()
if err != nil {
log.Error("parsing u2f registration: %v", err)
continue
}
newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter)
if authErr == nil {
reg.Counter = newCounter
user, err := user_model.GetUserByID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
remember := ctx.Session.Get("twofaRemember").(bool)
if err := reg.UpdateCounter(); err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if ctx.Session.Get("linkAccount") != nil {
if err := externalaccount.LinkAccountFromStore(ctx.Session, user); err != nil {
ctx.ServerError("UserSignIn", err)
return
}
}
redirect := handleSignInFull(ctx, user, remember, false)
if ctx.Written() {
return
}
if redirect == "" {
redirect = setting.AppSubURL + "/"
}
ctx.PlainText(http.StatusOK, redirect)
return
}
}
ctx.Error(http.StatusUnauthorized)
}

@ -0,0 +1,169 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"encoding/base64"
"errors"
"net/http"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/externalaccount"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
var tplWebAuthn base.TplName = "user/auth/webauthn"
// WebAuthn shows the WebAuthn login page
func WebAuthn(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
//Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
return
}
ctx.HTML(200, tplWebAuthn)
}
// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
func WebAuthnLoginAssertion(ctx *context.Context) {
// Ensure user is in a WebAuthn session.
idSess, ok := ctx.Session.Get("twofaUid").(int64)
if !ok || idSess == 0 {
ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
return
}
user, err := user_model.GetUserByID(idSess)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
exists, err := auth.ExistsWebAuthnCredentialsForUID(user.ID)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if !exists {
ctx.ServerError("UserSignIn", errors.New("no device registered"))
return
}
assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user), webauthn.WithAssertionExtensions(protocol.AuthenticationExtensions{
"appid": setting.U2F.AppID,
}))
if err != nil {
ctx.ServerError("webauthn.BeginLogin", err)
return
}
if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil {
ctx.ServerError("Session.Set", err)
return
}
ctx.JSON(http.StatusOK, assertion)
}
// WebAuthnLoginAssertionPost validates the signature and logs the user in
func WebAuthnLoginAssertionPost(ctx *context.Context) {
idSess, ok := ctx.Session.Get("twofaUid").(int64)
sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData)
if !ok || !okData || sessionData == nil || idSess == 0 {
ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
return
}
defer func() {
_ = ctx.Session.Delete("webauthnAssertion")
}()
// Load the user from the db
user, err := user_model.GetUserByID(idSess)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
log.Trace("Finishing webauthn authentication with user: %s", user.Name)
// Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
// (from webauthnAssertion) and verify the provided request.0
parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
ctx.Status(http.StatusForbidden)
return
}
// Validate the parsed response.
cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
ctx.Status(http.StatusForbidden)
return
}
// Ensure that the credential wasn't cloned by checking if CloneWarning is set.
// (This is set if the sign counter is less than the one we have stored.)
if cred.Authenticator.CloneWarning {
log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr())
ctx.Status(http.StatusForbidden)
return
}
// Success! Get the credential and update the sign count with the new value we received.
dbCred, err := auth.GetWebAuthnCredentialByCredID(base64.RawStdEncoding.EncodeToString(cred.ID))
if err != nil {
ctx.ServerError("GetWebAuthnCredentialByCredID", err)
return
}
dbCred.SignCount = cred.Authenticator.SignCount
if err := dbCred.UpdateSignCount(); err != nil {
ctx.ServerError("UpdateSignCount", err)
return
}
// Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil {
if err := externalaccount.LinkAccountFromStore(ctx.Session, user); err != nil {
ctx.ServerError("LinkAccountFromStore", err)
return
}
}
remember := ctx.Session.Get("twofaRemember").(bool)
redirect := handleSignInFull(ctx, user, remember, false)
if redirect == "" {
redirect = setting.AppSubURL + "/"
}
_ = ctx.Session.Delete("twofaUid")
// Finally check if the appid extension was used:
if value, ok := parsedResponse.ClientExtensionResults["appid"]; ok {
if appid, ok := value.(bool); ok && appid {
ctx.Flash.Error(ctx.Tr("webauthn_u2f_deprecated", dbCred.Name))
}
}
ctx.JSON(200, map[string]string{"redirect": redirect})
}

@ -63,11 +63,12 @@ func loadSecurityData(ctx *context.Context) {
} }
ctx.Data["TOTPEnrolled"] = enrolled ctx.Data["TOTPEnrolled"] = enrolled
ctx.Data["U2FRegistrations"], err = auth.GetU2FRegistrationsByUID(ctx.User.ID) credentials, err := auth.GetWebAuthnCredentialsByUID(ctx.User.ID)
if err != nil { if err != nil {
ctx.ServerError("GetU2FRegistrationsByUID", err) ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return return
} }
ctx.Data["WebAuthnCredentials"] = credentials
tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID}) tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
if err != nil { if err != nil {

@ -1,111 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package security
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/tstranex/u2f"
)
// U2FRegister initializes the u2f registration procedure
func U2FRegister(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.U2FRegistrationForm)
if form.Name == "" {
ctx.Error(http.StatusConflict)
return
}
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
if err != nil {
ctx.ServerError("NewChallenge", err)
return
}
if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
ctx.ServerError("Unable to set session key for u2fChallenge", err)
return
}
regs, err := auth.GetU2FRegistrationsByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("GetU2FRegistrationsByUID", err)
return
}
for _, reg := range regs {
if reg.Name == form.Name {
ctx.Error(http.StatusConflict, "Name already taken")
return
}
}
if err := ctx.Session.Set("u2fName", form.Name); err != nil {
ctx.ServerError("Unable to set session key for u2fName", err)
return
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
ctx.JSON(http.StatusOK, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations()))
}
// U2FRegisterPost receives the response of the security key
func U2FRegisterPost(ctx *context.Context) {
response := web.GetForm(ctx).(*u2f.RegisterResponse)
challSess := ctx.Session.Get("u2fChallenge")
u2fName := ctx.Session.Get("u2fName")
if challSess == nil || u2fName == nil {
ctx.ServerError("U2FRegisterPost", errors.New("not in U2F session"))
return
}
challenge := challSess.(*u2f.Challenge)
name := u2fName.(string)
config := &u2f.Config{
// Chrome 66+ doesn't return the device's attestation
// certificate by default.
SkipAttestationVerify: true,
}
reg, err := u2f.Register(*response, *challenge, config)
if err != nil {
ctx.ServerError("u2f.Register", err)
return
}
if _, err = auth.CreateRegistration(ctx.User.ID, name, reg); err != nil {
ctx.ServerError("u2f.Register", err)
return
}
ctx.Status(200)
}
// U2FDelete deletes an security key by id
func U2FDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.U2FDeleteForm)
reg, err := auth.GetU2FRegistrationByID(form.ID)
if err != nil {
if auth.IsErrU2FRegistrationNotExist(err) {
ctx.Status(200)
return
}
ctx.ServerError("GetU2FRegistrationByID", err)
return
}
if reg.UserID != ctx.User.ID {
ctx.Status(401)
return
}
if err := auth.DeleteRegistration(reg); err != nil {
ctx.ServerError("DeleteRegistration", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}

@ -0,0 +1,119 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package security
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/auth"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
// WebAuthnRegister initializes the webauthn registration procedure
func WebAuthnRegister(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm)
if form.Name == "" {
ctx.Error(http.StatusConflict)
return
}
cred, err := auth.GetWebAuthnCredentialByName(ctx.User.ID, form.Name)
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
if cred != nil {
ctx.Error(http.StatusConflict, "Name already taken")
return
}
_ = ctx.Session.Delete("registration")
if err := ctx.Session.Set("WebauthnName", form.Name); err != nil {
ctx.ServerError("Unable to set session key for WebauthnName", err)
return
}
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.User))
if err != nil {
ctx.ServerError("Unable to BeginRegistration", err)
return
}
// Save the session data as marshaled JSON
if err = ctx.Session.Set("registration", sessionData); err != nil {
ctx.ServerError("Unable to set session", err)
return
}
ctx.JSON(http.StatusOK, credentialOptions)
}
// WebauthnRegisterPost receives the response of the security key
func WebauthnRegisterPost(ctx *context.Context) {
name, ok := ctx.Session.Get("WebauthnName").(string)
if !ok || name == "" {
ctx.ServerError("Get WebauthnName", errors.New("no WebauthnName"))
return
}
// Load the session data
sessionData, ok := ctx.Session.Get("registration").(*webauthn.SessionData)
if !ok || sessionData == nil {
ctx.ServerError("Get registration", errors.New("no registration"))
return
}
defer func() {
_ = ctx.Session.Delete("registration")
}()
// Verify that the challenge succeeded
cred, err := wa.WebAuthn.FinishRegistration((*wa.User)(ctx.User), *sessionData, ctx.Req)
if err != nil {
if pErr, ok := err.(*protocol.Error); ok {
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)
}
ctx.ServerError("CreateCredential", err)
return
}
dbCred, err := auth.GetWebAuthnCredentialByName(ctx.User.ID, name)
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
if dbCred != nil {
ctx.Error(http.StatusConflict, "Name already taken")
return
}
// Create the credential
_, err = auth.CreateCredential(ctx.User.ID, name, cred)
if err != nil {
ctx.ServerError("CreateCredential", err)
return
}
ctx.JSON(http.StatusCreated, cred)
}
// WebauthnDelete deletes an security key by id
func WebauthnDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.WebauthnDeleteForm)
if _, err := auth.DeleteCredential(form.ID, ctx.User.ID); err != nil {
ctx.ServerError("GetWebAuthnCredentialByID", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}

@ -5,7 +5,6 @@
package web package web
import ( import (
"encoding/gob"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -45,7 +44,6 @@ import (
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/tstranex/u2f"
) )
const ( const (
@ -99,8 +97,6 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), 301) http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), 301)
}) })
gob.Register(&u2f.Challenge{})
common := []interface{}{} common := []interface{}{}
if setting.EnableGzip { if setting.EnableGzip {
@ -290,11 +286,10 @@ func RegisterRoutes(m *web.Route) {
m.Get("/scratch", auth.TwoFactorScratch) m.Get("/scratch", auth.TwoFactorScratch)
m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), auth.TwoFactorScratchPost) m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), auth.TwoFactorScratchPost)
}) })
m.Group("/u2f", func() { m.Group("/webauthn", func() {
m.Get("", auth.U2F) m.Get("", auth.WebAuthn)
m.Get("/challenge", auth.U2FChallenge) m.Get("/assertion", auth.WebAuthnLoginAssertion)
m.Post("/sign", bindIgnErr(u2f.SignResponse{}), auth.U2FSign) m.Post("/assertion", auth.WebAuthnLoginAssertionPost)
}) })
}, reqSignOut) }, reqSignOut)
@ -337,10 +332,10 @@ func RegisterRoutes(m *web.Route) {
m.Get("/enroll", security.EnrollTwoFactor) m.Get("/enroll", security.EnrollTwoFactor)
m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), security.EnrollTwoFactorPost) m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), security.EnrollTwoFactorPost)
}) })
m.Group("/u2f", func() { m.Group("/webauthn", func() {
m.Post("/request_register", bindIgnErr(forms.U2FRegistrationForm{}), security.U2FRegister) m.Post("/request_register", bindIgnErr(forms.WebauthnRegistrationForm{}), security.WebAuthnRegister)
m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), security.U2FRegisterPost) m.Post("/register", security.WebauthnRegisterPost)
m.Post("/delete", bindIgnErr(forms.U2FDeleteForm{}), security.U2FDelete) m.Post("/delete", bindIgnErr(forms.WebauthnDeleteForm{}), security.WebauthnDelete)
}) })
m.Group("/openid", func() { m.Group("/openid", func() {
m.Post("", bindIgnErr(forms.AddOpenIDForm{}), security.OpenIDPost) m.Post("", bindIgnErr(forms.AddOpenIDForm{}), security.OpenIDPost)

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -69,6 +70,8 @@ func Init() {
log.Error("Could not initialize '%s' auth method, error: %s", reflect.TypeOf(method).String(), err) log.Error("Could not initialize '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
} }
} }
webauthn.Init()
} }
// Free should be called exactly once when the application is terminating to allow Auth plugins // Free should be called exactly once when the application is terminating to allow Auth plugins
@ -121,7 +124,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
_ = sess.Delete("openid_determined_username") _ = sess.Delete("openid_determined_username")
_ = sess.Delete("twofaUid") _ = sess.Delete("twofaUid")
_ = sess.Delete("twofaRemember") _ = sess.Delete("twofaRemember")
_ = sess.Delete("u2fChallenge") _ = sess.Delete("webauthnAssertion")
_ = sess.Delete("linkAccount") _ = sess.Delete("linkAccount")
err = sess.Set("uid", user.ID) err = sess.Set("uid", user.ID)
if err != nil { if err != nil {

@ -25,7 +25,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
ini "gopkg.in/ini.v1" ini "gopkg.in/ini.v1"
) )

@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
) )
// ___________ __ // ___________ __
@ -37,6 +37,7 @@ type Token struct {
GrantID int64 `json:"gnt"` GrantID int64 `json:"gnt"`
Type TokenType `json:"tt"` Type TokenType `json:"tt"`
Counter int64 `json:"cnt,omitempty"` Counter int64 `json:"cnt,omitempty"`
// FIXME: Migrate to registered claims
jwt.StandardClaims jwt.StandardClaims
} }
@ -69,6 +70,7 @@ func (token *Token) SignToken(signingKey JWTSigningKey) (string, error) {
// OIDCToken represents an OpenID Connect id_token // OIDCToken represents an OpenID Connect id_token
type OIDCToken struct { type OIDCToken struct {
// FIXME: Migrate to RegisteredClaims
jwt.StandardClaims jwt.StandardClaims
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`

@ -409,24 +409,24 @@ func (f *TwoFactorScratchAuthForm) Validate(req *http.Request, errs binding.Erro
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
// U2FRegistrationForm for reserving an U2F name // WebauthnRegistrationForm for reserving an WebAuthn name
type U2FRegistrationForm struct { type WebauthnRegistrationForm struct {
Name string `binding:"Required"` Name string `binding:"Required"`
} }
// Validate validates the fields // Validate validates the fields
func (f *U2FRegistrationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *WebauthnRegistrationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req) ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
// U2FDeleteForm for deleting U2F keys // WebauthnDeleteForm for deleting WebAuthn keys
type U2FDeleteForm struct { type WebauthnDeleteForm struct {
ID int64 `binding:"Required"` ID int64 `binding:"Required"`
} }
// Validate validates the fields // Validate validates the fields
func (f *U2FDeleteForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *WebauthnDeleteForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req) ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }

@ -30,7 +30,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt/v4"
) )
// requestContext contain variables from the HTTP request. // requestContext contain variables from the HTTP request.
@ -45,6 +45,7 @@ type Claims struct {
RepoID int64 RepoID int64
Op string Op string
UserID int64 UserID int64
// FIXME: Migrate to RegisteredClaims
jwt.StandardClaims jwt.StandardClaims
} }

@ -14,9 +14,6 @@
{{template "base/footer_content" .}} {{template "base/footer_content" .}}
<!-- Third-party libraries --> <!-- Third-party libraries -->
{{if .RequireU2F}}
<script src="{{AssetUrlPrefix}}/vendor/plugins/u2f/index.js"></script>
{{end}}
{{if .EnableCaptcha}} {{if .EnableCaptcha}}
{{if eq .CaptchaType "recaptcha"}} {{if eq .CaptchaType "recaptcha"}}
<script src='{{ URLJoin .RecaptchaURL "api.js"}}' async></script> <script src='{{ URLJoin .RecaptchaURL "api.js"}}' async></script>

@ -1,24 +0,0 @@
{{template "base/head" .}}
<div class="page-content user signin">
<div class="ui middle centered very relaxed page grid">
<div class="column">
<h3 class="ui top attached header">
{{.i18n.Tr "twofa"}}
</h3>
<div class="ui attached segment">
<i class="huge key icon"></i>
<h3>{{.i18n.Tr "u2f_insert_key"}}</h3>
{{template "base/alert" .}}
<p>{{.i18n.Tr "u2f_sign_in"}}</p>
</div>
<div id="wait-for-key" class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "u2f_press_button"}} </div>
{{if .TOTPEnrolled}}
<div class="ui attached segment">
<a href="{{AppSubUrl}}/user/two_factor">{{.i18n.Tr "u2f_use_twofa"}}</a>
</div>
{{end}}
</div>
</div>
</div>
{{template "user/auth/u2f_error" .}}
{{template "base/footer" .}}

@ -1,32 +0,0 @@
<div class="ui small modal" id="u2f-error">
<div class="header">{{.i18n.Tr "u2f_error"}}</div>
<div class="content">
<div class="ui negative message">
<div class="header">
{{.i18n.Tr "u2f_error"}}
</div>
<div class="hide" id="unsupported-browser">
{{.i18n.Tr "u2f_unsupported_browser"}}
</div>
<div class="hide" id="u2f-error-1">
{{.i18n.Tr "u2f_error_1"}}
</div>
<div class="hide" id="u2f-error-2">
{{.i18n.Tr "u2f_error_2"}}
</div>
<div class="hide" id="u2f-error-3">
{{.i18n.Tr "u2f_error_3"}}
</div>
<div class="hide" id="u2f-error-4">
{{.i18n.Tr "u2f_error_4"}}
</div>
<div class="hide u2f_error_5">
{{.i18n.Tr "u2f_error_5"}}
</div>
</div>
</div>
<div class="actions">
<button onclick="window.location.reload()" class="success ui button hide u2f_error_5">{{.i18n.Tr "u2f_reload"}}</button>
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div>
</div>
</div>

@ -0,0 +1,22 @@
{{template "base/head" .}}
<div class="user signin webauthn-prompt">
<div class="ui middle centered very relaxed page grid">
<div class="column">
<h3 class="ui top attached header">
{{.i18n.Tr "twofa"}}
</h3>
<div class="ui attached segment">
<i class="huge key icon"></i>
<h3>{{.i18n.Tr "webauthn_insert_key"}}</h3>
{{template "base/alert" .}}
<p>{{.i18n.Tr "webauthn_sign_in"}}</p>
</div>
<div class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "webauthn_press_button"}} </div>
<div class="ui attached segment">
<a href="{{AppSubUrl}}/user/two_factor">{{.i18n.Tr "webauthn_use_twofa"}}</a>
</div>
</div>
</div>
</div>
{{template "user/auth/webauthn_error" .}}
{{template "base/footer" .}}

@ -0,0 +1,22 @@
<div class="ui small modal" id="webauthn-error">
<div class="header">{{.i18n.Tr "webauthn_error"}}</div>
<div class="content">
<div class="ui negative message">
<div class="header">
{{.i18n.Tr "webauthn_error"}}
</div>
<div class="hide" data-webauthn-error-msg="browser"><p>{{.i18n.Tr "webauthn_unsupported_browser"}}</div>
<div class="hide" data-webauthn-error-msg="unknown"><p>{{.i18n.Tr "webauthn_error_unknown"}}</div>
<div class="hide" data-webauthn-error-msg="insecure"><p>{{.i18n.Tr "webauthn_error_insecure"}}</div>
<div class="hide" data-webauthn-error-msg="unable-to-process"><p>{{.i18n.Tr "webauthn_error_unable_to_process"}}</div>
<div class="hide" data-webauthn-error-msg="duplicated"><p>{{.i18n.Tr "webauthn_error_duplicated"}}</div>
<div class="hide" data-webauthn-error-msg="empty"><p>{{.i18n.Tr "webauthn_error_empty"}}</div>
<div class="hide" data-webauthn-error-msg="timeout"><p>{{.i18n.Tr "webauthn_error_timeout"}}</div>
<div class="hide" data-webauthn-error-msg="0"></div>
</div>
</div>
<div class="actions">
<button onclick="window.location.reload()" class="success ui button hide webauthn_error_timeout">{{.i18n.Tr "webauthn_reload"}}</button>
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div>
</div>
</div>

@ -4,7 +4,7 @@
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
{{template "user/settings/security/twofa" .}} {{template "user/settings/security/twofa" .}}
{{template "user/settings/security/u2f" .}} {{template "user/settings/security/webauthn" .}}
{{template "user/settings/security/accountlinks" .}} {{template "user/settings/security/accountlinks" .}}
{{if .EnableOpenIDSignIn}} {{if .EnableOpenIDSignIn}}
{{template "user/settings/security/openid" .}} {{template "user/settings/security/openid" .}}

@ -1,51 +1,52 @@
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{.i18n.Tr "settings.u2f"}} {{.i18n.Tr "settings.webauthn"}}
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
<p>{{.i18n.Tr "settings.u2f_desc" | Str2html}}</p> <p>{{.i18n.Tr "settings.webauthn_desc" | Str2html}}</p>
<div class="ui key list"> <div class="ui key list">
{{range .U2FRegistrations}} {{range .WebAuthnCredentials}}
<div class="item"> <div class="item">
<div class="right floated content"> <div class="right floated content">
<button class="ui red tiny button delete-button" data-modal-id="delete-registration" data-url="{{$.Link}}/u2f/delete" data-id="{{.ID}}"> <button class="ui red tiny button delete-button" data-modal-id="delete-registration" data-url="{{$.Link}}/webauthn/delete" data-id="{{.ID}}">
{{$.i18n.Tr "settings.delete_key"}} {{$.i18n.Tr "settings.delete_key"}}
</button> </button>
</div> </div>
<div class="content"> <div class="content">
<strong>{{.Name}}</strong> <strong>{{.Name}}</strong>
</div> </div>
<span class="time">{{TimeSinceUnix .CreatedUnix $.Lang}}</span>
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="ui form"> <div class="ui form">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div class="required field"> <div class="required field">
<label for="nickname">{{.i18n.Tr "settings.u2f_nickname"}}</label> <label for="nickname">{{.i18n.Tr "settings.webauthn_nickname"}}</label>
<input id="nickname" name="nickname" type="text" required> <input id="nickname" name="nickname" type="text" required>
</div> </div>
<button id="register-security-key" class="ui green button">{{svg "octicon-key"}} {{.i18n.Tr "settings.u2f_register_key"}}</button> <button id="register-webauthn" class="ui green button">{{svg "octicon-key"}} {{.i18n.Tr "settings.webauthn_register_key"}}</button>
</div> </div>
</div> </div>
<div class="ui small modal" id="register-device"> <div class="ui small modal" id="register-device">
<div class="header">{{.i18n.Tr "settings.u2f_register_key"}}</div> <div class="header">{{.i18n.Tr "settings.webauthn_register_key"}}</div>
<div class="content"> <div class="content">
<i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.u2f_press_button"}} <i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.webauthn_press_button"}}
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div> <div class="ui cancel button">{{.i18n.Tr "cancel"}}</div>
</div> </div>
</div> </div>
{{template "user/auth/u2f_error" .}} {{template "user/auth/webauthn_error" .}}
<div class="ui small basic delete modal" id="delete-registration"> <div class="ui small basic delete modal" id="delete-registration">
<div class="ui icon header"> <div class="ui icon header">
{{svg "octicon-trash"}} {{svg "octicon-trash"}}
{{.i18n.Tr "settings.u2f_delete_key"}} {{.i18n.Tr "settings.webauthn_delete_key"}}
</div> </div>
<div class="content"> <div class="content">
<p>{{.i18n.Tr "settings.u2f_delete_key_desc"}}</p> <p>{{.i18n.Tr "settings.webauthn_delete_key_desc"}}</p>
</div> </div>
{{template "base/delete_modal_actions" .}} {{template "base/delete_modal_actions" .}}
</div> </div>

@ -0,0 +1,24 @@
Copyright (c) 2014 CloudFlare Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,188 @@
// Package pkcs7 implements the subset of the CMS PKCS #7 datatype that is typically
// used to package certificates and CRLs. Using openssl, every certificate converted
// to PKCS #7 format from another encoding such as PEM conforms to this implementation.
// reference: https://www.openssl.org/docs/man1.1.0/apps/crl2pkcs7.html
//
// PKCS #7 Data type, reference: https://tools.ietf.org/html/rfc2315
//
// The full pkcs#7 cryptographic message syntax allows for cryptographic enhancements,
// for example data can be encrypted and signed and then packaged through pkcs#7 to be
// sent over a network and then verified and decrypted. It is asn1, and the type of
// PKCS #7 ContentInfo, which comprises the PKCS #7 structure, is:
//
// ContentInfo ::= SEQUENCE {
// contentType ContentType,
// content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL
// }
//
// There are 6 possible ContentTypes, data, signedData, envelopedData,
// signedAndEnvelopedData, digestedData, and encryptedData. Here signedData, Data, and encrypted
// Data are implemented, as the degenerate case of signedData without a signature is the typical
// format for transferring certificates and CRLS, and Data and encryptedData are used in PKCS #12
// formats.
// The ContentType signedData has the form:
//
//
// signedData ::= SEQUENCE {
// version Version,
// digestAlgorithms DigestAlgorithmIdentifiers,
// contentInfo ContentInfo,
// certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL
// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
// signerInfos SignerInfos
// }
//
// As of yet signerInfos and digestAlgorithms are not parsed, as they are not relevant to
// this system's use of PKCS #7 data. Version is an integer type, note that PKCS #7 is
// recursive, this second layer of ContentInfo is similar ignored for our degenerate
// usage. The ExtendedCertificatesAndCertificates type consists of a sequence of choices
// between PKCS #6 extended certificates and x509 certificates. Any sequence consisting
// of any number of extended certificates is not yet supported in this implementation.
//
// The ContentType Data is simply a raw octet string and is parsed directly into a Go []byte slice.
//
// The ContentType encryptedData is the most complicated and its form can be gathered by
// the go type below. It essentially contains a raw octet string of encrypted data and an
// algorithm identifier for use in decrypting this data.
package pkcs7
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
cferr "github.com/cloudflare/cfssl/errors"
)
// Types used for asn1 Unmarshaling.
type signedData struct {
Version int
DigestAlgorithms asn1.RawValue
ContentInfo asn1.RawValue
Certificates asn1.RawValue `asn1:"optional" asn1:"tag:0"`
Crls asn1.RawValue `asn1:"optional"`
SignerInfos asn1.RawValue
}
type initPKCS7 struct {
Raw asn1.RawContent
ContentType asn1.ObjectIdentifier
Content asn1.RawValue `asn1:"tag:0,explicit,optional"`
}
// Object identifier strings of the three implemented PKCS7 types.
const (
ObjIDData = "1.2.840.113549.1.7.1"
ObjIDSignedData = "1.2.840.113549.1.7.2"
ObjIDEncryptedData = "1.2.840.113549.1.7.6"
)
// PKCS7 represents the ASN1 PKCS #7 Content type. It contains one of three
// possible types of Content objects, as denoted by the object identifier in
// the ContentInfo field, the other two being nil. SignedData
// is the degenerate SignedData Content info without signature used
// to hold certificates and crls. Data is raw bytes, and EncryptedData
// is as defined in PKCS #7 standard.
type PKCS7 struct {
Raw asn1.RawContent
ContentInfo string
Content Content
}
// Content implements three of the six possible PKCS7 data types. Only one is non-nil.
type Content struct {
Data []byte
SignedData SignedData
EncryptedData EncryptedData
}
// SignedData defines the typical carrier of certificates and crls.
type SignedData struct {
Raw asn1.RawContent
Version int
Certificates []*x509.Certificate
Crl *pkix.CertificateList
}
// Data contains raw bytes. Used as a subtype in PKCS12.
type Data struct {
Bytes []byte
}
// EncryptedData contains encrypted data. Used as a subtype in PKCS12.
type EncryptedData struct {
Raw asn1.RawContent
Version int
EncryptedContentInfo EncryptedContentInfo
}
// EncryptedContentInfo is a subtype of PKCS7EncryptedData.
type EncryptedContentInfo struct {
Raw asn1.RawContent
ContentType asn1.ObjectIdentifier
ContentEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedContent []byte `asn1:"tag:0,optional"`
}
// ParsePKCS7 attempts to parse the DER encoded bytes of a
// PKCS7 structure.
func ParsePKCS7(raw []byte) (msg *PKCS7, err error) {
var pkcs7 initPKCS7
_, err = asn1.Unmarshal(raw, &pkcs7)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
}
msg = new(PKCS7)
msg.Raw = pkcs7.Raw
msg.ContentInfo = pkcs7.ContentType.String()
switch {
case msg.ContentInfo == ObjIDData:
msg.ContentInfo = "Data"
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &msg.Content.Data)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
}
case msg.ContentInfo == ObjIDSignedData:
msg.ContentInfo = "SignedData"
var signedData signedData
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &signedData)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
}
if len(signedData.Certificates.Bytes) != 0 {
msg.Content.SignedData.Certificates, err = x509.ParseCertificates(signedData.Certificates.Bytes)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
}
}
if len(signedData.Crls.Bytes) != 0 {
msg.Content.SignedData.Crl, err = x509.ParseDERCRL(signedData.Crls.Bytes)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
}
}
msg.Content.SignedData.Version = signedData.Version
msg.Content.SignedData.Raw = pkcs7.Content.Bytes
case msg.ContentInfo == ObjIDEncryptedData:
msg.ContentInfo = "EncryptedData"
var encryptedData EncryptedData
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &encryptedData)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
}
if encryptedData.Version != 0 {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("Only support for PKCS #7 encryptedData version 0"))
}
msg.Content.EncryptedData = encryptedData
default:
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("Attempt to parse PKCS# 7 Content not of type data, signed data or encrypted data"))
}
return msg, nil
}

@ -0,0 +1,46 @@
/*
Package errors provides error types returned in CF SSL.
1. Type Error is intended for errors produced by CF SSL packages.
It formats to a json object that consists of an error message and a 4-digit code for error reasoning.
Example: {"code":1002, "message": "Failed to decode certificate"}
The index of codes are listed below:
1XXX: CertificateError
1000: Unknown
1001: ReadFailed
1002: DecodeFailed
1003: ParseFailed
1100: SelfSigned
12XX: VerifyFailed
121X: CertificateInvalid
1210: NotAuthorizedToSign
1211: Expired
1212: CANotAuthorizedForThisName
1213: TooManyIntermediates
1214: IncompatibleUsage
1220: UnknownAuthority
2XXX: PrivatekeyError
2000: Unknown
2001: ReadFailed
2002: DecodeFailed
2003: ParseFailed
2100: Encrypted
2200: NotRSA
2300: KeyMismatch
2400: GenerationFailed
2500: Unavailable
3XXX: IntermediatesError
4XXX: RootError
5XXX: PolicyError
5100: NoKeyUsages
5200: InvalidPolicy
5300: InvalidRequest
5400: UnknownProfile
6XXX: DialError
2. Type HttpError is intended for CF SSL API to consume. It contains a HTTP status code that will be read and returned
by the API server.
*/
package errors

@ -0,0 +1,438 @@
package errors
import (
"crypto/x509"
"encoding/json"
"fmt"
)
// Error is the error type usually returned by functions in CF SSL package.
// It contains a 4-digit error code where the most significant digit
// describes the category where the error occurred and the rest 3 digits
// describe the specific error reason.
type Error struct {
ErrorCode int `json:"code"`
Message string `json:"message"`
}
// Category is the most significant digit of the error code.
type Category int
// Reason is the last 3 digits of the error code.
type Reason int
const (
// Success indicates no error occurred.
Success Category = 1000 * iota // 0XXX
// CertificateError indicates a fault in a certificate.
CertificateError // 1XXX
// PrivateKeyError indicates a fault in a private key.
PrivateKeyError // 2XXX
// IntermediatesError indicates a fault in an intermediate.
IntermediatesError // 3XXX
// RootError indicates a fault in a root.
RootError // 4XXX
// PolicyError indicates an error arising from a malformed or
// non-existent policy, or a breach of policy.
PolicyError // 5XXX
// DialError indicates a network fault.
DialError // 6XXX
// APIClientError indicates a problem with the API client.
APIClientError // 7XXX
// OCSPError indicates a problem with OCSP signing
OCSPError // 8XXX
// CSRError indicates a problem with CSR parsing
CSRError // 9XXX
// CTError indicates a problem with the certificate transparency process
CTError // 10XXX
// CertStoreError indicates a problem with the certificate store
CertStoreError // 11XXX
)
// None is a non-specified error.
const (
None Reason = iota
)
// Warning code for a success
const (
BundleExpiringBit int = 1 << iota // 0x01
BundleNotUbiquitousBit // 0x02
)
// Parsing errors
const (
Unknown Reason = iota // X000
ReadFailed // X001
DecodeFailed // X002
ParseFailed // X003
)
// The following represent certificate non-parsing errors, and must be
// specified along with CertificateError.
const (
// SelfSigned indicates that a certificate is self-signed and
// cannot be used in the manner being attempted.
SelfSigned Reason = 100 * (iota + 1) // Code 11XX
// VerifyFailed is an X.509 verification failure. The least two
// significant digits of 12XX is determined as the actual x509
// error is examined.
VerifyFailed // Code 12XX
// BadRequest indicates that the certificate request is invalid.
BadRequest // Code 13XX
// MissingSerial indicates that the profile specified
// 'ClientProvidesSerialNumbers', but the SignRequest did not include a serial
// number.
MissingSerial // Code 14XX
)
const (
certificateInvalid = 10 * (iota + 1) //121X
unknownAuthority //122x
)
// The following represent private-key non-parsing errors, and must be
// specified with PrivateKeyError.
const (
// Encrypted indicates that the private key is a PKCS #8 encrypted
// private key. At this time, CFSSL does not support decrypting
// these keys.
Encrypted Reason = 100 * (iota + 1) //21XX
// NotRSAOrECC indicates that they key is not an RSA or ECC
// private key; these are the only two private key types supported
// at this time by CFSSL.
NotRSAOrECC //22XX
// KeyMismatch indicates that the private key does not match
// the public key or certificate being presented with the key.
KeyMismatch //23XX
// GenerationFailed indicates that a private key could not
// be generated.
GenerationFailed //24XX
// Unavailable indicates that a private key mechanism (such as
// PKCS #11) was requested but support for that mechanism is
// not available.
Unavailable
)
// The following are policy-related non-parsing errors, and must be
// specified along with PolicyError.
const (
// NoKeyUsages indicates that the profile does not permit any
// key usages for the certificate.
NoKeyUsages Reason = 100 * (iota + 1) // 51XX
// InvalidPolicy indicates that policy being requested is not
// a valid policy or does not exist.
InvalidPolicy // 52XX
// InvalidRequest indicates a certificate request violated the
// constraints of the policy being applied to the request.
InvalidRequest // 53XX
// UnknownProfile indicates that the profile does not exist.
UnknownProfile // 54XX
UnmatchedWhitelist // 55xx
)
// The following are API client related errors, and should be
// specified with APIClientError.
const (
// AuthenticationFailure occurs when the client is unable
// to obtain an authentication token for the request.
AuthenticationFailure Reason = 100 * (iota + 1)
// JSONError wraps an encoding/json error.
JSONError
// IOError wraps an io/ioutil error.
IOError
// ClientHTTPError wraps a net/http error.
ClientHTTPError
// ServerRequestFailed covers any other failures from the API
// client.
ServerRequestFailed
)
// The following are OCSP related errors, and should be
// specified with OCSPError
const (
// IssuerMismatch ocurs when the certificate in the OCSP signing
// request was not issued by the CA that this responder responds for.
IssuerMismatch Reason = 100 * (iota + 1) // 81XX
// InvalidStatus occurs when the OCSP signing requests includes an
// invalid value for the certificate status.
InvalidStatus
)
// Certificate transparency related errors specified with CTError
const (
// PrecertSubmissionFailed occurs when submitting a precertificate to
// a log server fails
PrecertSubmissionFailed = 100 * (iota + 1)
// CTClientConstructionFailed occurs when the construction of a new
// github.com/google/certificate-transparency client fails.
CTClientConstructionFailed
// PrecertMissingPoison occurs when a precert is passed to SignFromPrecert
// and is missing the CT poison extension.
PrecertMissingPoison
// PrecertInvalidPoison occurs when a precert is passed to SignFromPrecert
// and has a invalid CT poison extension value or the extension is not
// critical.
PrecertInvalidPoison
)
// Certificate persistence related errors specified with CertStoreError
const (
// InsertionFailed occurs when a SQL insert query failes to complete.
InsertionFailed = 100 * (iota + 1)
// RecordNotFound occurs when a SQL query targeting on one unique
// record failes to update the specified row in the table.
RecordNotFound
)
// The error interface implementation, which formats to a JSON object string.
func (e *Error) Error() string {
marshaled, err := json.Marshal(e)
if err != nil {
panic(err)
}
return string(marshaled)
}
// New returns an error that contains an error code and message derived from
// the given category, reason. Currently, to avoid confusion, it is not
// allowed to create an error of category Success
func New(category Category, reason Reason) *Error {
errorCode := int(category) + int(reason)
var msg string
switch category {
case OCSPError:
switch reason {
case ReadFailed:
msg = "No certificate provided"
case IssuerMismatch:
msg = "Certificate not issued by this issuer"
case InvalidStatus:
msg = "Invalid revocation status"
}
case CertificateError:
switch reason {
case Unknown:
msg = "Unknown certificate error"
case ReadFailed:
msg = "Failed to read certificate"
case DecodeFailed:
msg = "Failed to decode certificate"
case ParseFailed:
msg = "Failed to parse certificate"
case SelfSigned:
msg = "Certificate is self signed"
case VerifyFailed:
msg = "Unable to verify certificate"
case BadRequest:
msg = "Invalid certificate request"
case MissingSerial:
msg = "Missing serial number in request"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category CertificateError.",
reason))
}
case PrivateKeyError:
switch reason {
case Unknown:
msg = "Unknown private key error"
case ReadFailed:
msg = "Failed to read private key"
case DecodeFailed:
msg = "Failed to decode private key"
case ParseFailed:
msg = "Failed to parse private key"
case Encrypted:
msg = "Private key is encrypted."
case NotRSAOrECC:
msg = "Private key algorithm is not RSA or ECC"
case KeyMismatch:
msg = "Private key does not match public key"
case GenerationFailed:
msg = "Failed to new private key"
case Unavailable:
msg = "Private key is unavailable"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category PrivateKeyError.",
reason))
}
case IntermediatesError:
switch reason {
case Unknown:
msg = "Unknown intermediate certificate error"
case ReadFailed:
msg = "Failed to read intermediate certificate"
case DecodeFailed:
msg = "Failed to decode intermediate certificate"
case ParseFailed:
msg = "Failed to parse intermediate certificate"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category IntermediatesError.",
reason))
}
case RootError:
switch reason {
case Unknown:
msg = "Unknown root certificate error"
case ReadFailed:
msg = "Failed to read root certificate"
case DecodeFailed:
msg = "Failed to decode root certificate"
case ParseFailed:
msg = "Failed to parse root certificate"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category RootError.",
reason))
}
case PolicyError:
switch reason {
case Unknown:
msg = "Unknown policy error"
case NoKeyUsages:
msg = "Invalid policy: no key usage available"
case InvalidPolicy:
msg = "Invalid or unknown policy"
case InvalidRequest:
msg = "Policy violation request"
case UnknownProfile:
msg = "Unknown policy profile"
case UnmatchedWhitelist:
msg = "Request does not match policy whitelist"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category PolicyError.",
reason))
}
case DialError:
switch reason {
case Unknown:
msg = "Failed to dial remote server"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category DialError.",
reason))
}
case APIClientError:
switch reason {
case AuthenticationFailure:
msg = "API client authentication failure"
case JSONError:
msg = "API client JSON config error"
case ClientHTTPError:
msg = "API client HTTP error"
case IOError:
msg = "API client IO error"
case ServerRequestFailed:
msg = "API client error: Server request failed"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category APIClientError.",
reason))
}
case CSRError:
switch reason {
case Unknown:
msg = "CSR parsing failed due to unknown error"
case ReadFailed:
msg = "CSR file read failed"
case ParseFailed:
msg = "CSR Parsing failed"
case DecodeFailed:
msg = "CSR Decode failed"
case BadRequest:
msg = "CSR Bad request"
default:
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category APIClientError.", reason))
}
case CTError:
switch reason {
case Unknown:
msg = "Certificate transparency parsing failed due to unknown error"
case PrecertSubmissionFailed:
msg = "Certificate transparency precertificate submission failed"
case PrecertMissingPoison:
msg = "Precertificate is missing CT poison extension"
case PrecertInvalidPoison:
msg = "Precertificate contains an invalid CT poison extension"
default:
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category CTError.", reason))
}
case CertStoreError:
switch reason {
case Unknown:
msg = "Certificate store action failed due to unknown error"
default:
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category CertStoreError.", reason))
}
default:
panic(fmt.Sprintf("Unsupported CFSSL error type: %d.",
category))
}
return &Error{ErrorCode: errorCode, Message: msg}
}
// Wrap returns an error that contains the given error and an error code derived from
// the given category, reason and the error. Currently, to avoid confusion, it is not
// allowed to create an error of category Success
func Wrap(category Category, reason Reason, err error) *Error {
errorCode := int(category) + int(reason)
if err == nil {
panic("Wrap needs a supplied error to initialize.")
}
// do not double wrap a error
switch err.(type) {
case *Error:
panic("Unable to wrap a wrapped error.")
}
switch category {
case CertificateError:
// given VerifyFailed , report the status with more detailed status code
// for some certificate errors we care.
if reason == VerifyFailed {
switch errorType := err.(type) {
case x509.CertificateInvalidError:
errorCode += certificateInvalid + int(errorType.Reason)
case x509.UnknownAuthorityError:
errorCode += unknownAuthority
}
}
case PrivateKeyError, IntermediatesError, RootError, PolicyError, DialError,
APIClientError, CSRError, CTError, CertStoreError, OCSPError:
// no-op, just use the error
default:
panic(fmt.Sprintf("Unsupported CFSSL error type: %d.",
category))
}
return &Error{ErrorCode: errorCode, Message: err.Error()}
}

@ -0,0 +1,47 @@
package errors
import (
"errors"
"net/http"
)
// HTTPError is an augmented error with a HTTP status code.
type HTTPError struct {
StatusCode int
error
}
// Error implements the error interface.
func (e *HTTPError) Error() string {
return e.error.Error()
}
// NewMethodNotAllowed returns an appropriate error in the case that
// an HTTP client uses an invalid method (i.e. a GET in place of a POST)
// on an API endpoint.
func NewMethodNotAllowed(method string) *HTTPError {
return &HTTPError{http.StatusMethodNotAllowed, errors.New(`Method is not allowed:"` + method + `"`)}
}
// NewBadRequest creates a HttpError with the given error and error code 400.
func NewBadRequest(err error) *HTTPError {
return &HTTPError{http.StatusBadRequest, err}
}
// NewBadRequestString returns a HttpError with the supplied message
// and error code 400.
func NewBadRequestString(s string) *HTTPError {
return NewBadRequest(errors.New(s))
}
// NewBadRequestMissingParameter returns a 400 HttpError as a required
// parameter is missing in the HTTP request.
func NewBadRequestMissingParameter(s string) *HTTPError {
return NewBadRequestString(`Missing parameter "` + s + `"`)
}
// NewBadRequestUnwantedParameter returns a 400 HttpError as a unnecessary
// parameter is present in the HTTP request.
func NewBadRequestUnwantedParameter(s string) *HTTPError {
return NewBadRequestString(`Unwanted parameter "` + s + `"`)
}

@ -0,0 +1,48 @@
// Package derhelpers implements common functionality
// on DER encoded data
package derhelpers
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
cferr "github.com/cloudflare/cfssl/errors"
"golang.org/x/crypto/ed25519"
)
// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, or Ed25519 DER-encoded
// private key. The key must not be in PEM format.
func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) {
generalKey, err := x509.ParsePKCS8PrivateKey(keyDER)
if err != nil {
generalKey, err = x509.ParsePKCS1PrivateKey(keyDER)
if err != nil {
generalKey, err = x509.ParseECPrivateKey(keyDER)
if err != nil {
generalKey, err = ParseEd25519PrivateKey(keyDER)
if err != nil {
// We don't include the actual error into
// the final error. The reason might be
// we don't want to leak any info about
// the private key.
return nil, cferr.New(cferr.PrivateKeyError,
cferr.ParseFailed)
}
}
}
}
switch generalKey.(type) {
case *rsa.PrivateKey:
return generalKey.(*rsa.PrivateKey), nil
case *ecdsa.PrivateKey:
return generalKey.(*ecdsa.PrivateKey), nil
case ed25519.PrivateKey:
return generalKey.(ed25519.PrivateKey), nil
}
// should never reach here
return nil, cferr.New(cferr.PrivateKeyError, cferr.ParseFailed)
}

@ -0,0 +1,133 @@
package derhelpers
import (
"crypto"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"golang.org/x/crypto/ed25519"
)
var errEd25519WrongID = errors.New("incorrect object identifier")
var errEd25519WrongKeyType = errors.New("incorrect key type")
// ed25519OID is the OID for the Ed25519 signature scheme: see
// https://datatracker.ietf.org/doc/draft-ietf-curdle-pkix-04.
var ed25519OID = asn1.ObjectIdentifier{1, 3, 101, 112}
// subjectPublicKeyInfo reflects the ASN.1 object defined in the X.509 standard.
//
// This is defined in crypto/x509 as "publicKeyInfo".
type subjectPublicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
// MarshalEd25519PublicKey creates a DER-encoded SubjectPublicKeyInfo for an
// ed25519 public key, as defined in
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04. This is analagous to
// MarshalPKIXPublicKey in crypto/x509, which doesn't currently support Ed25519.
func MarshalEd25519PublicKey(pk crypto.PublicKey) ([]byte, error) {
pub, ok := pk.(ed25519.PublicKey)
if !ok {
return nil, errEd25519WrongKeyType
}
spki := subjectPublicKeyInfo{
Algorithm: pkix.AlgorithmIdentifier{
Algorithm: ed25519OID,
},
PublicKey: asn1.BitString{
BitLength: len(pub) * 8,
Bytes: pub,
},
}
return asn1.Marshal(spki)
}
// ParseEd25519PublicKey returns the Ed25519 public key encoded by the input.
func ParseEd25519PublicKey(der []byte) (crypto.PublicKey, error) {
var spki subjectPublicKeyInfo
if rest, err := asn1.Unmarshal(der, &spki); err != nil {
return nil, err
} else if len(rest) > 0 {
return nil, errors.New("SubjectPublicKeyInfo too long")
}
if !spki.Algorithm.Algorithm.Equal(ed25519OID) {
return nil, errEd25519WrongID
}
if spki.PublicKey.BitLength != ed25519.PublicKeySize*8 {
return nil, errors.New("SubjectPublicKeyInfo PublicKey length mismatch")
}
return ed25519.PublicKey(spki.PublicKey.Bytes), nil
}
// oneAsymmetricKey reflects the ASN.1 structure for storing private keys in
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04, excluding the optional
// fields, which we don't use here.
//
// This is identical to pkcs8 in crypto/x509.
type oneAsymmetricKey struct {
Version int
Algorithm pkix.AlgorithmIdentifier
PrivateKey []byte
}
// curvePrivateKey is the innter type of the PrivateKey field of
// oneAsymmetricKey.
type curvePrivateKey []byte
// MarshalEd25519PrivateKey returns a DER encdoing of the input private key as
// specified in https://tools.ietf.org/html/draft-ietf-curdle-pkix-04.
func MarshalEd25519PrivateKey(sk crypto.PrivateKey) ([]byte, error) {
priv, ok := sk.(ed25519.PrivateKey)
if !ok {
return nil, errEd25519WrongKeyType
}
// Marshal the innter CurvePrivateKey.
curvePrivateKey, err := asn1.Marshal(priv.Seed())
if err != nil {
return nil, err
}
// Marshal the OneAsymmetricKey.
asym := oneAsymmetricKey{
Version: 0,
Algorithm: pkix.AlgorithmIdentifier{
Algorithm: ed25519OID,
},
PrivateKey: curvePrivateKey,
}
return asn1.Marshal(asym)
}
// ParseEd25519PrivateKey returns the Ed25519 private key encoded by the input.
func ParseEd25519PrivateKey(der []byte) (crypto.PrivateKey, error) {
asym := new(oneAsymmetricKey)
if rest, err := asn1.Unmarshal(der, asym); err != nil {
return nil, err
} else if len(rest) > 0 {
return nil, errors.New("OneAsymmetricKey too long")
}
// Check that the key type is correct.
if !asym.Algorithm.Algorithm.Equal(ed25519OID) {
return nil, errEd25519WrongID
}
// Unmarshal the inner CurvePrivateKey.
seed := new(curvePrivateKey)
if rest, err := asn1.Unmarshal(asym.PrivateKey, seed); err != nil {
return nil, err
} else if len(rest) > 0 {
return nil, errors.New("CurvePrivateKey too long")
}
return ed25519.NewKeyFromSeed(*seed), nil
}

@ -0,0 +1,590 @@
// Package helpers implements utility functionality common to many
// CFSSL packages.
package helpers
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/google/certificate-transparency-go"
cttls "github.com/google/certificate-transparency-go/tls"
ctx509 "github.com/google/certificate-transparency-go/x509"
"golang.org/x/crypto/ocsp"
"strings"
"time"
"github.com/cloudflare/cfssl/crypto/pkcs7"
cferr "github.com/cloudflare/cfssl/errors"
"github.com/cloudflare/cfssl/helpers/derhelpers"
"github.com/cloudflare/cfssl/log"
"golang.org/x/crypto/pkcs12"
)
// OneYear is a time.Duration representing a year's worth of seconds.
const OneYear = 8760 * time.Hour
// OneDay is a time.Duration representing a day's worth of seconds.
const OneDay = 24 * time.Hour
// InclusiveDate returns the time.Time representation of a date - 1
// nanosecond. This allows time.After to be used inclusively.
func InclusiveDate(year int, month time.Month, day int) time.Time {
return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(-1 * time.Nanosecond)
}
// Jul2012 is the July 2012 CAB Forum deadline for when CAs must stop
// issuing certificates valid for more than 5 years.
var Jul2012 = InclusiveDate(2012, time.July, 01)
// Apr2015 is the April 2015 CAB Forum deadline for when CAs must stop
// issuing certificates valid for more than 39 months.
var Apr2015 = InclusiveDate(2015, time.April, 01)
// KeyLength returns the bit size of ECDSA or RSA PublicKey
func KeyLength(key interface{}) int {
if key == nil {
return 0
}
if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok {
return ecdsaKey.Curve.Params().BitSize
} else if rsaKey, ok := key.(*rsa.PublicKey); ok {
return rsaKey.N.BitLen()
}
return 0
}
// ExpiryTime returns the time when the certificate chain is expired.
func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) {
if len(chain) == 0 {
return
}
notAfter = chain[0].NotAfter
for _, cert := range chain {
if notAfter.After(cert.NotAfter) {
notAfter = cert.NotAfter
}
}
return
}
// MonthsValid returns the number of months for which a certificate is valid.
func MonthsValid(c *x509.Certificate) int {
issued := c.NotBefore
expiry := c.NotAfter
years := (expiry.Year() - issued.Year())
months := years*12 + int(expiry.Month()) - int(issued.Month())
// Round up if valid for less than a full month
if expiry.Day() > issued.Day() {
months++
}
return months
}
// ValidExpiry determines if a certificate is valid for an acceptable
// length of time per the CA/Browser Forum baseline requirements.
// See https://cabforum.org/wp-content/uploads/CAB-Forum-BR-1.3.0.pdf
func ValidExpiry(c *x509.Certificate) bool {
issued := c.NotBefore
var maxMonths int
switch {
case issued.After(Apr2015):
maxMonths = 39
case issued.After(Jul2012):
maxMonths = 60
case issued.Before(Jul2012):
maxMonths = 120
}
if MonthsValid(c) > maxMonths {
return false
}
return true
}
// SignatureString returns the TLS signature string corresponding to
// an X509 signature algorithm.
func SignatureString(alg x509.SignatureAlgorithm) string {
switch alg {
case x509.MD2WithRSA:
return "MD2WithRSA"
case x509.MD5WithRSA:
return "MD5WithRSA"
case x509.SHA1WithRSA:
return "SHA1WithRSA"
case x509.SHA256WithRSA:
return "SHA256WithRSA"
case x509.SHA384WithRSA:
return "SHA384WithRSA"
case x509.SHA512WithRSA:
return "SHA512WithRSA"
case x509.DSAWithSHA1:
return "DSAWithSHA1"
case x509.DSAWithSHA256:
return "DSAWithSHA256"
case x509.ECDSAWithSHA1:
return "ECDSAWithSHA1"
case x509.ECDSAWithSHA256:
return "ECDSAWithSHA256"
case x509.ECDSAWithSHA384:
return "ECDSAWithSHA384"
case x509.ECDSAWithSHA512:
return "ECDSAWithSHA512"
default:
return "Unknown Signature"
}
}
// HashAlgoString returns the hash algorithm name contains in the signature
// method.
func HashAlgoString(alg x509.SignatureAlgorithm) string {
switch alg {
case x509.MD2WithRSA:
return "MD2"
case x509.MD5WithRSA:
return "MD5"
case x509.SHA1WithRSA:
return "SHA1"
case x509.SHA256WithRSA:
return "SHA256"
case x509.SHA384WithRSA:
return "SHA384"
case x509.SHA512WithRSA:
return "SHA512"
case x509.DSAWithSHA1:
return "SHA1"
case x509.DSAWithSHA256:
return "SHA256"
case x509.ECDSAWithSHA1:
return "SHA1"
case x509.ECDSAWithSHA256:
return "SHA256"
case x509.ECDSAWithSHA384:
return "SHA384"
case x509.ECDSAWithSHA512:
return "SHA512"
default:
return "Unknown Hash Algorithm"
}
}
// StringTLSVersion returns underlying enum values from human names for TLS
// versions, defaults to current golang default of TLS 1.0
func StringTLSVersion(version string) uint16 {
switch version {
case "1.2":
return tls.VersionTLS12
case "1.1":
return tls.VersionTLS11
default:
return tls.VersionTLS10
}
}
// EncodeCertificatesPEM encodes a number of x509 certificates to PEM
func EncodeCertificatesPEM(certs []*x509.Certificate) []byte {
var buffer bytes.Buffer
for _, cert := range certs {
pem.Encode(&buffer, &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
}
return buffer.Bytes()
}
// EncodeCertificatePEM encodes a single x509 certificates to PEM
func EncodeCertificatePEM(cert *x509.Certificate) []byte {
return EncodeCertificatesPEM([]*x509.Certificate{cert})
}
// ParseCertificatesPEM parses a sequence of PEM-encoded certificate and returns them,
// can handle PEM encoded PKCS #7 structures.
func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
var err error
certsPEM = bytes.TrimSpace(certsPEM)
for len(certsPEM) > 0 {
var cert []*x509.Certificate
cert, certsPEM, err = ParseOneCertificateFromPEM(certsPEM)
if err != nil {
return nil, cferr.New(cferr.CertificateError, cferr.ParseFailed)
} else if cert == nil {
break
}
certs = append(certs, cert...)
}
if len(certsPEM) > 0 {
return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
}
return certs, nil
}
// ParseCertificatesDER parses a DER encoding of a certificate object and possibly private key,
// either PKCS #7, PKCS #12, or raw x509.
func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) {
certsDER = bytes.TrimSpace(certsDER)
pkcs7data, err := pkcs7.ParsePKCS7(certsDER)
if err != nil {
var pkcs12data interface{}
certs = make([]*x509.Certificate, 1)
pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password)
if err != nil {
certs, err = x509.ParseCertificates(certsDER)
if err != nil {
return nil, nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
}
} else {
key = pkcs12data.(crypto.Signer)
}
} else {
if pkcs7data.ContentInfo != "SignedData" {
return nil, nil, cferr.Wrap(cferr.CertificateError, cferr.DecodeFailed, errors.New("can only extract certificates from signed data content info"))
}
certs = pkcs7data.Content.SignedData.Certificates
}
if certs == nil {
return nil, key, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
}
return certs, key, nil
}
// ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed.
func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
cert, err := ParseCertificatePEM(certPEM)
if err != nil {
return nil, err
}
if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.VerifyFailed, err)
}
return cert, nil
}
// ParseCertificatePEM parses and returns a PEM-encoded certificate,
// can handle PEM encoded PKCS #7 structures.
func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
certPEM = bytes.TrimSpace(certPEM)
cert, rest, err := ParseOneCertificateFromPEM(certPEM)
if err != nil {
// Log the actual parsing error but throw a default parse error message.
log.Debugf("Certificate parsing error: %v", err)
return nil, cferr.New(cferr.CertificateError, cferr.ParseFailed)
} else if cert == nil {
return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
} else if len(rest) > 0 {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PEM file should contain only one object"))
} else if len(cert) > 1 {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PKCS7 object in the PEM file should contain only one certificate"))
}
return cert[0], nil
}
// ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object,
// either a raw x509 certificate or a PKCS #7 structure possibly containing
// multiple certificates, from the top of certsPEM, which itself may
// contain multiple PEM encoded certificate objects.
func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) {
block, rest := pem.Decode(certsPEM)
if block == nil {
return nil, rest, nil
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
pkcs7data, err := pkcs7.ParsePKCS7(block.Bytes)
if err != nil {
return nil, rest, err
}
if pkcs7data.ContentInfo != "SignedData" {
return nil, rest, errors.New("only PKCS #7 Signed Data Content Info supported for certificate parsing")
}
certs := pkcs7data.Content.SignedData.Certificates
if certs == nil {
return nil, rest, errors.New("PKCS #7 structure contains no certificates")
}
return certs, rest, nil
}
var certs = []*x509.Certificate{cert}
return certs, rest, nil
}
// LoadPEMCertPool loads a pool of PEM certificates from file.
func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) {
if certsFile == "" {
return nil, nil
}
pemCerts, err := ioutil.ReadFile(certsFile)
if err != nil {
return nil, err
}
return PEMToCertPool(pemCerts)
}
// PEMToCertPool concerts PEM certificates to a CertPool.
func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) {
if len(pemCerts) == 0 {
return nil, nil
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(pemCerts) {
return nil, errors.New("failed to load cert pool")
}
return certPool, nil
}
// ParsePrivateKeyPEM parses and returns a PEM-encoded private
// key. The private key may be either an unencrypted PKCS#8, PKCS#1,
// or elliptic private key.
func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) {
return ParsePrivateKeyPEMWithPassword(keyPEM, nil)
}
// ParsePrivateKeyPEMWithPassword parses and returns a PEM-encoded private
// key. The private key may be a potentially encrypted PKCS#8, PKCS#1,
// or elliptic private key.
func ParsePrivateKeyPEMWithPassword(keyPEM []byte, password []byte) (key crypto.Signer, err error) {
keyDER, err := GetKeyDERFromPEM(keyPEM, password)
if err != nil {
return nil, err
}
return derhelpers.ParsePrivateKeyDER(keyDER)
}
// GetKeyDERFromPEM parses a PEM-encoded private key and returns DER-format key bytes.
func GetKeyDERFromPEM(in []byte, password []byte) ([]byte, error) {
keyDER, _ := pem.Decode(in)
if keyDER != nil {
if procType, ok := keyDER.Headers["Proc-Type"]; ok {
if strings.Contains(procType, "ENCRYPTED") {
if password != nil {
return x509.DecryptPEMBlock(keyDER, password)
}
return nil, cferr.New(cferr.PrivateKeyError, cferr.Encrypted)
}
}
return keyDER.Bytes, nil
}
return nil, cferr.New(cferr.PrivateKeyError, cferr.DecodeFailed)
}
// ParseCSR parses a PEM- or DER-encoded PKCS #10 certificate signing request.
func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error) {
in = bytes.TrimSpace(in)
p, rest := pem.Decode(in)
if p != nil {
if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" {
return nil, rest, cferr.New(cferr.CSRError, cferr.BadRequest)
}
csr, err = x509.ParseCertificateRequest(p.Bytes)
} else {
csr, err = x509.ParseCertificateRequest(in)
}
if err != nil {
return nil, rest, err
}
err = csr.CheckSignature()
if err != nil {
return nil, rest, err
}
return csr, rest, nil
}
// ParseCSRPEM parses a PEM-encoded certificate signing request.
// It does not check the signature. This is useful for dumping data from a CSR
// locally.
func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) {
block, _ := pem.Decode([]byte(csrPEM))
if block == nil {
return nil, cferr.New(cferr.CSRError, cferr.DecodeFailed)
}
csrObject, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, err
}
return csrObject, nil
}
// SignerAlgo returns an X.509 signature algorithm from a crypto.Signer.
func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm {
switch pub := priv.Public().(type) {
case *rsa.PublicKey:
bitLength := pub.N.BitLen()
switch {
case bitLength >= 4096:
return x509.SHA512WithRSA
case bitLength >= 3072:
return x509.SHA384WithRSA
case bitLength >= 2048:
return x509.SHA256WithRSA
default:
return x509.SHA1WithRSA
}
case *ecdsa.PublicKey:
switch pub.Curve {
case elliptic.P521():
return x509.ECDSAWithSHA512
case elliptic.P384():
return x509.ECDSAWithSHA384
case elliptic.P256():
return x509.ECDSAWithSHA256
default:
return x509.ECDSAWithSHA1
}
default:
return x509.UnknownSignatureAlgorithm
}
}
// LoadClientCertificate load key/certificate from pem files
func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) {
if certFile != "" && keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Criticalf("Unable to read client certificate from file: %s or key from file: %s", certFile, keyFile)
return nil, err
}
log.Debug("Client certificate loaded ")
return &cert, nil
}
return nil, nil
}
// CreateTLSConfig creates a tls.Config object from certs and roots
func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config {
var certs []tls.Certificate
if cert != nil {
certs = []tls.Certificate{*cert}
}
return &tls.Config{
Certificates: certs,
RootCAs: remoteCAs,
}
}
// SerializeSCTList serializes a list of SCTs.
func SerializeSCTList(sctList []ct.SignedCertificateTimestamp) ([]byte, error) {
list := ctx509.SignedCertificateTimestampList{}
for _, sct := range sctList {
sctBytes, err := cttls.Marshal(sct)
if err != nil {
return nil, err
}
list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes})
}
return cttls.Marshal(list)
}
// DeserializeSCTList deserializes a list of SCTs.
func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) {
var sctList ctx509.SignedCertificateTimestampList
rest, err := cttls.Unmarshal(serializedSCTList, &sctList)
if err != nil {
return nil, err
}
if len(rest) != 0 {
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, errors.New("serialized SCT list contained trailing garbage"))
}
list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList))
for i, serializedSCT := range sctList.SCTList {
var sct ct.SignedCertificateTimestamp
rest, err := cttls.Unmarshal(serializedSCT.Val, &sct)
if err != nil {
return nil, err
}
if len(rest) != 0 {
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, errors.New("serialized SCT contained trailing garbage"))
}
list[i] = sct
}
return list, nil
}
// SCTListFromOCSPResponse extracts the SCTList from an ocsp.Response,
// returning an empty list if the SCT extension was not found or could not be
// unmarshalled.
func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTimestamp, error) {
// This loop finds the SCTListExtension in the OCSP response.
var SCTListExtension, ext pkix.Extension
for _, ext = range response.Extensions {
// sctExtOid is the ObjectIdentifier of a Signed Certificate Timestamp.
sctExtOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5}
if ext.Id.Equal(sctExtOid) {
SCTListExtension = ext
break
}
}
// This code block extracts the sctList from the SCT extension.
var sctList []ct.SignedCertificateTimestamp
var err error
if numBytes := len(SCTListExtension.Value); numBytes != 0 {
var serializedSCTList []byte
rest := make([]byte, numBytes)
copy(rest, SCTListExtension.Value)
for len(rest) != 0 {
rest, err = asn1.Unmarshal(rest, &serializedSCTList)
if err != nil {
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, err)
}
}
sctList, err = DeserializeSCTList(serializedSCTList)
}
return sctList, err
}
// ReadBytes reads a []byte either from a file or an environment variable.
// If valFile has a prefix of 'env:', the []byte is read from the environment
// using the subsequent name. If the prefix is 'file:' the []byte is read from
// the subsequent file. If no prefix is provided, valFile is assumed to be a
// file path.
func ReadBytes(valFile string) ([]byte, error) {
switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) {
case 1:
return ioutil.ReadFile(valFile)
case 2:
switch splitVal[0] {
case "env":
return []byte(os.Getenv(splitVal[1])), nil
case "file":
return ioutil.ReadFile(splitVal[1])
default:
return nil, fmt.Errorf("unknown prefix: %s", splitVal[0])
}
default:
return nil, fmt.Errorf("multiple prefixes: %s",
strings.Join(splitVal[:len(splitVal)-1], ", "))
}
}

@ -0,0 +1,162 @@
// Package log implements a wrapper around the Go standard library's
// logging package. Clients should set the current log level; only
// messages below that level will actually be logged. For example, if
// Level is set to LevelWarning, only log messages at the Warning,
// Error, and Critical levels will be logged.
package log
import (
"fmt"
"log"
"os"
)
// The following constants represent logging levels in increasing levels of seriousness.
const (
// LevelDebug is the log level for Debug statements.
LevelDebug = iota
// LevelInfo is the log level for Info statements.
LevelInfo
// LevelWarning is the log level for Warning statements.
LevelWarning
// LevelError is the log level for Error statements.
LevelError
// LevelCritical is the log level for Critical statements.
LevelCritical
// LevelFatal is the log level for Fatal statements.
LevelFatal
)
var levelPrefix = [...]string{
LevelDebug: "DEBUG",
LevelInfo: "INFO",
LevelWarning: "WARNING",
LevelError: "ERROR",
LevelCritical: "CRITICAL",
LevelFatal: "FATAL",
}
// Level stores the current logging level.
var Level = LevelInfo
// SyslogWriter specifies the necessary methods for an alternate output
// destination passed in via SetLogger.
//
// SyslogWriter is satisfied by *syslog.Writer.
type SyslogWriter interface {
Debug(string)
Info(string)
Warning(string)
Err(string)
Crit(string)
Emerg(string)
}
// syslogWriter stores the SetLogger() parameter.
var syslogWriter SyslogWriter
// SetLogger sets the output used for output by this package.
// A *syslog.Writer is a good choice for the logger parameter.
// Call with a nil parameter to revert to default behavior.
func SetLogger(logger SyslogWriter) {
syslogWriter = logger
}
func print(l int, msg string) {
if l >= Level {
if syslogWriter != nil {
switch l {
case LevelDebug:
syslogWriter.Debug(msg)
case LevelInfo:
syslogWriter.Info(msg)
case LevelWarning:
syslogWriter.Warning(msg)
case LevelError:
syslogWriter.Err(msg)
case LevelCritical:
syslogWriter.Crit(msg)
case LevelFatal:
syslogWriter.Emerg(msg)
}
} else {
log.Printf("[%s] %s", levelPrefix[l], msg)
}
}
}
func outputf(l int, format string, v []interface{}) {
print(l, fmt.Sprintf(format, v...))
}
func output(l int, v []interface{}) {
print(l, fmt.Sprint(v...))
}
// Fatalf logs a formatted message at the "fatal" level and then exits. The
// arguments are handled in the same manner as fmt.Printf.
func Fatalf(format string, v ...interface{}) {
outputf(LevelFatal, format, v)
os.Exit(1)
}
// Fatal logs its arguments at the "fatal" level and then exits.
func Fatal(v ...interface{}) {
output(LevelFatal, v)
os.Exit(1)
}
// Criticalf logs a formatted message at the "critical" level. The
// arguments are handled in the same manner as fmt.Printf.
func Criticalf(format string, v ...interface{}) {
outputf(LevelCritical, format, v)
}
// Critical logs its arguments at the "critical" level.
func Critical(v ...interface{}) {
output(LevelCritical, v)
}
// Errorf logs a formatted message at the "error" level. The arguments
// are handled in the same manner as fmt.Printf.
func Errorf(format string, v ...interface{}) {
outputf(LevelError, format, v)
}
// Error logs its arguments at the "error" level.
func Error(v ...interface{}) {
output(LevelError, v)
}
// Warningf logs a formatted message at the "warning" level. The
// arguments are handled in the same manner as fmt.Printf.
func Warningf(format string, v ...interface{}) {
outputf(LevelWarning, format, v)
}
// Warning logs its arguments at the "warning" level.
func Warning(v ...interface{}) {
output(LevelWarning, v)
}
// Infof logs a formatted message at the "info" level. The arguments
// are handled in the same manner as fmt.Printf.
func Infof(format string, v ...interface{}) {
outputf(LevelInfo, format, v)
}
// Info logs its arguments at the "info" level.
func Info(v ...interface{}) {
output(LevelInfo, v)
}
// Debugf logs a formatted message at the "debug" level. The arguments
// are handled in the same manner as fmt.Printf.
func Debugf(format string, v ...interface{}) {
outputf(LevelDebug, format, v)
}
// Debug logs its arguments at the "debug" level.
func Debug(v ...interface{}) {
output(LevelDebug, v)
}

@ -0,0 +1,336 @@
// Package revoke provides functionality for checking the validity of
// a cert. Specifically, the temporal validity of the certificate is
// checked first, then any CRL and OCSP url in the cert is checked.
package revoke
import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
neturl "net/url"
"sync"
"time"
"golang.org/x/crypto/ocsp"
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/log"
)
// HardFail determines whether the failure to check the revocation
// status of a certificate (i.e. due to network failure) causes
// verification to fail (a hard failure).
var HardFail = false
// CRLSet associates a PKIX certificate list with the URL the CRL is
// fetched from.
var CRLSet = map[string]*pkix.CertificateList{}
var crlLock = new(sync.Mutex)
// We can't handle LDAP certificates, so this checks to see if the
// URL string points to an LDAP resource so that we can ignore it.
func ldapURL(url string) bool {
u, err := neturl.Parse(url)
if err != nil {
log.Warningf("error parsing url %s: %v", url, err)
return false
}
if u.Scheme == "ldap" {
return true
}
return false
}
// revCheck should check the certificate for any revocations. It
// returns a pair of booleans: the first indicates whether the certificate
// is revoked, the second indicates whether the revocations were
// successfully checked.. This leads to the following combinations:
//
// false, false: an error was encountered while checking revocations.
//
// false, true: the certificate was checked successfully and
// it is not revoked.
//
// true, true: the certificate was checked successfully and
// it is revoked.
//
// true, false: failure to check revocation status causes
// verification to fail
func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) {
for _, url := range cert.CRLDistributionPoints {
if ldapURL(url) {
log.Infof("skipping LDAP CRL: %s", url)
continue
}
if revoked, ok, err := certIsRevokedCRL(cert, url); !ok {
log.Warning("error checking revocation via CRL")
if HardFail {
return true, false, err
}
return false, false, err
} else if revoked {
log.Info("certificate is revoked via CRL")
return true, true, err
}
}
if revoked, ok, err := certIsRevokedOCSP(cert, HardFail); !ok {
log.Warning("error checking revocation via OCSP")
if HardFail {
return true, false, err
}
return false, false, err
} else if revoked {
log.Info("certificate is revoked via OCSP")
return true, true, err
}
return false, true, nil
}
// fetchCRL fetches and parses a CRL.
func fetchCRL(url string) (*pkix.CertificateList, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
} else if resp.StatusCode >= 300 {
return nil, errors.New("failed to retrieve CRL")
}
body, err := crlRead(resp.Body)
if err != nil {
return nil, err
}
resp.Body.Close()
return x509.ParseCRL(body)
}
func getIssuer(cert *x509.Certificate) *x509.Certificate {
var issuer *x509.Certificate
var err error
for _, issuingCert := range cert.IssuingCertificateURL {
issuer, err = fetchRemote(issuingCert)
if err != nil {
continue
}
break
}
return issuer
}
// check a cert against a specific CRL. Returns the same bool pair
// as revCheck, plus an error if one occurred.
func certIsRevokedCRL(cert *x509.Certificate, url string) (revoked, ok bool, err error) {
crl, ok := CRLSet[url]
if ok && crl == nil {
ok = false
crlLock.Lock()
delete(CRLSet, url)
crlLock.Unlock()
}
var shouldFetchCRL = true
if ok {
if !crl.HasExpired(time.Now()) {
shouldFetchCRL = false
}
}
issuer := getIssuer(cert)
if shouldFetchCRL {
var err error
crl, err = fetchCRL(url)
if err != nil {
log.Warningf("failed to fetch CRL: %v", err)
return false, false, err
}
// check CRL signature
if issuer != nil {
err = issuer.CheckCRLSignature(crl)
if err != nil {
log.Warningf("failed to verify CRL: %v", err)
return false, false, err
}
}
crlLock.Lock()
CRLSet[url] = crl
crlLock.Unlock()
}
for _, revoked := range crl.TBSCertList.RevokedCertificates {
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
log.Info("Serial number match: intermediate is revoked.")
return true, true, err
}
}
return false, true, err
}
// VerifyCertificate ensures that the certificate passed in hasn't
// expired and checks the CRL for the server.
func VerifyCertificate(cert *x509.Certificate) (revoked, ok bool) {
revoked, ok, _ = VerifyCertificateError(cert)
return revoked, ok
}
// VerifyCertificateError ensures that the certificate passed in hasn't
// expired and checks the CRL for the server.
func VerifyCertificateError(cert *x509.Certificate) (revoked, ok bool, err error) {
if !time.Now().Before(cert.NotAfter) {
msg := fmt.Sprintf("Certificate expired %s\n", cert.NotAfter)
log.Info(msg)
return true, true, fmt.Errorf(msg)
} else if !time.Now().After(cert.NotBefore) {
msg := fmt.Sprintf("Certificate isn't valid until %s\n", cert.NotBefore)
log.Info(msg)
return true, true, fmt.Errorf(msg)
}
return revCheck(cert)
}
func fetchRemote(url string) (*x509.Certificate, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
in, err := remoteRead(resp.Body)
if err != nil {
return nil, err
}
resp.Body.Close()
p, _ := pem.Decode(in)
if p != nil {
return helpers.ParseCertificatePEM(in)
}
return x509.ParseCertificate(in)
}
var ocspOpts = ocsp.RequestOptions{
Hash: crypto.SHA1,
}
func certIsRevokedOCSP(leaf *x509.Certificate, strict bool) (revoked, ok bool, e error) {
var err error
ocspURLs := leaf.OCSPServer
if len(ocspURLs) == 0 {
// OCSP not enabled for this certificate.
return false, true, nil
}
issuer := getIssuer(leaf)
if issuer == nil {
return false, false, nil
}
ocspRequest, err := ocsp.CreateRequest(leaf, issuer, &ocspOpts)
if err != nil {
return revoked, ok, err
}
for _, server := range ocspURLs {
resp, err := sendOCSPRequest(server, ocspRequest, leaf, issuer)
if err != nil {
if strict {
return revoked, ok, err
}
continue
}
// There wasn't an error fetching the OCSP status.
ok = true
if resp.Status != ocsp.Good {
// The certificate was revoked.
revoked = true
}
return revoked, ok, err
}
return revoked, ok, err
}
// sendOCSPRequest attempts to request an OCSP response from the
// server. The error only indicates a failure to *fetch* the
// certificate, and *does not* mean the certificate is valid.
func sendOCSPRequest(server string, req []byte, leaf, issuer *x509.Certificate) (*ocsp.Response, error) {
var resp *http.Response
var err error
if len(req) > 256 {
buf := bytes.NewBuffer(req)
resp, err = http.Post(server, "application/ocsp-request", buf)
} else {
reqURL := server + "/" + neturl.QueryEscape(base64.StdEncoding.EncodeToString(req))
resp, err = http.Get(reqURL)
}
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to retrieve OSCP")
}
body, err := ocspRead(resp.Body)
if err != nil {
return nil, err
}
resp.Body.Close()
switch {
case bytes.Equal(body, ocsp.UnauthorizedErrorResponse):
return nil, errors.New("OSCP unauthorized")
case bytes.Equal(body, ocsp.MalformedRequestErrorResponse):
return nil, errors.New("OSCP malformed")
case bytes.Equal(body, ocsp.InternalErrorErrorResponse):
return nil, errors.New("OSCP internal error")
case bytes.Equal(body, ocsp.TryLaterErrorResponse):
return nil, errors.New("OSCP try later")
case bytes.Equal(body, ocsp.SigRequredErrorResponse):
return nil, errors.New("OSCP signature required")
}
return ocsp.ParseResponseForCert(body, leaf, issuer)
}
var crlRead = ioutil.ReadAll
// SetCRLFetcher sets the function to use to read from the http response body
func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) {
crlRead = fn
}
var remoteRead = ioutil.ReadAll
// SetRemoteFetcher sets the function to use to read from the http response body
func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) {
remoteRead = fn
}
var ocspRead = ioutil.ReadAll
// SetOCSPFetcher sets the function to use to read from the http response body
func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) {
ocspRead = fn
}

@ -0,0 +1,26 @@
Copyright (c) 2017 Duo Security, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,617 @@
package metadata
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/cloudflare/cfssl/revoke"
"github.com/mitchellh/mapstructure"
uuid "github.com/satori/go.uuid"
jwt "github.com/golang-jwt/jwt/v4"
)
// Metadata is a map of authenticator AAGUIDs to corresponding metadata statements
var Metadata = make(map[uuid.UUID]MetadataTOCPayloadEntry)
// Conformance indicates if test metadata is currently being used
var Conformance = false
// AuthenticatorAttestationType - The ATTESTATION constants are 16 bit long integers indicating the specific attestation that authenticator supports.
type AuthenticatorAttestationType uint16
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 shouldt be done according to [UAFMetadataService].
BasicFull AuthenticatorAttestationType = 0x3E07
// 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
// 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
// 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
)
// 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).
type AuthenticatorStatus string
const (
// NotFidoCertified - This authenticator is not FIDO certified.
NotFidoCertified = "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 = "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 = "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 = "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 = "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 = "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 = "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 = "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 StatusReport.url.
SelfAssertionSubmitted = "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 = "FIDO_CERTIFIED_L1"
// FidoCertifiedL1plus - The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level 1.
FidoCertifiedL1plus = "FIDO_CERTIFIED_L1plus"
// FidoCertifiedL2 - The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level 1+.
FidoCertifiedL2 = "FIDO_CERTIFIED_L2"
// FidoCertifiedL2plus - The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level 2.
FidoCertifiedL2plus = "FIDO_CERTIFIED_L2plus"
// FidoCertifiedL3 - The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level 2+.
FidoCertifiedL3 = "FIDO_CERTIFIED_L3"
// FidoCertifiedL3plus - The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level 3.
FidoCertifiedL3plus = "FIDO_CERTIFIED_L3plus"
)
// UndesiredAuthenticatorStatus is an array of undesirable authenticator statuses
var UndesiredAuthenticatorStatus = [...]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 UndesiredAuthenticatorStatus {
if s == status {
return true
}
}
return false
}
// StatusReport - Contains the current BiometricStatusReport of one of the authenticator's biometric component.
type StatusReport struct {
// Status of the authenticator. Additional fields MAY be set depending on this value.
Status string `json:"status"`
// ISO-8601 formatted date since when the status code was set, if applicable. If no date is given, the status is assumed to be effective while present.
EffectiveDate string `json:"effectiveDate"`
// Base64-encoded [RFC4648] (not base64url!) DER [ITU-X690-2008] PKIX certificate value related to the current status, if applicable.
Certificate string `json:"certificate"`
// HTTPS URL where additional information may be found related to the current status, if applicable.
URL string `json:"url"`
// Describes the externally visible aspects of the Authenticator Certification evaluation.
CertificationDescriptor string `json:"certificationDescriptor"`
// The unique identifier for the issued Certification.
CertificateNumber string `json:"certificateNumber"`
// The version of the Authenticator Certification Policy the implementation is Certified to, e.g. "1.0.0".
CertificationPolicyVersion string `json:"certificationPolicyVersion"`
// The Document Version of the Authenticator Security Requirements (DV) [FIDOAuthenticatorSecurityRequirements] the implementation is certified to, e.g. "1.2.0".
CertificationRequirementsVersion string `json:"certificationRequirementsVersion"`
}
// BiometricStatusReport - Contains the current BiometricStatusReport of one of the authenticator's biometric component.
type BiometricStatusReport struct {
// Achieved level of the biometric certification of this biometric component of the authenticator
CertLevel uint16 `json:"certLevel"`
// A single USER_VERIFY constant indicating the modality of the biometric component
Modality uint32 `json:"modality"`
// ISO-8601 formatted date since when the certLevel achieved, if applicable. If no date is given, the status is assumed to be effective while present.
EffectiveDate string `json:"effectiveDate"`
// Describes the externally visible aspects of the Biometric Certification evaluation.
CertificationDescriptor string `json:"certificationDescriptor"`
// The unique identifier for the issued Biometric Certification.
CertificateNumber string `json:"certificateNumber"`
// The version of the Biometric Certification Policy the implementation is Certified to, e.g. "1.0.0".
CertificationPolicyVersion string `json:"certificationPolicyVersion"`
// The version of the Biometric Requirements [FIDOBiometricsRequirements] the implementation is certified to, e.g. "1.0.0".
CertificationRequirementsVersion string `json:"certificationRequirementsVersion"`
}
// MetadataTOCPayloadEntry - Represents the MetadataTOCPayloadEntry
type MetadataTOCPayloadEntry struct {
// The AAID of the authenticator this metadata TOC payload entry relates to.
Aaid string `json:"aaid"`
// The Authenticator Attestation GUID.
AaGUID string `json:"aaguid"`
// A list of the attestation certificate public key identifiers encoded as hex string.
AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"`
// The hash value computed over the base64url encoding of the UTF-8 representation of the JSON encoded metadata statement available at url and as defined in [FIDOMetadataStatement].
Hash string `json:"hash"`
// Uniform resource locator (URL) of the encoded metadata statement for this authenticator model (identified by its AAID, AAGUID or attestationCertificateKeyIdentifier).
URL string `json:"url"`
// Status of the FIDO Biometric Certification of one or more biometric components of the Authenticator
BiometricStatusReports []BiometricStatusReport `json:"biometricStatusReports"`
// An array of status reports applicable to this authenticator.
StatusReports []StatusReport `json:"statusReports"`
// ISO-8601 formatted date since when the status report array was set to the current value.
TimeOfLastStatusChange string `json:"timeOfLastStatusChange"`
// URL of a list of rogue (i.e. untrusted) individual authenticators.
RogueListURL string `json:"rogueListURL"`
// The hash value computed over the Base64url encoding of the UTF-8 representation of the JSON encoded rogueList available at rogueListURL (with type rogueListEntry[]).
RogueListHash string `json:"rogueListHash"`
MetadataStatement MetadataStatement
}
// RogueListEntry - Contains a list of individual authenticators known to be rogue
type RogueListEntry struct {
// Base64url encoding of the rogue authenticator's secret key
Sk string `json:"sk"`
// ISO-8601 formatted date since when this entry is effective.
Date string `json:"date"`
}
// MetadataTOCPayload - Represents the MetadataTOCPayload
type MetadataTOCPayload struct {
// The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement.
LegalHeader string `json:"legalHeader"`
// The serial number of this UAF Metadata TOC Payload. Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor TOC will have a no value exactly incremented by one.
Number int `json:"no"`
// ISO-8601 formatted date when the next update will be provided at latest.
NextUpdate string `json:"nextUpdate"`
// List of zero or more MetadataTOCPayloadEntry objects.
Entries []MetadataTOCPayloadEntry `json:"entries"`
}
// Version - Represents a generic version with major and minor fields.
type Version struct {
// Major version.
Major uint16 `json:"major"`
// Minor version.
Minor uint16 `json:"minor"`
}
// CodeAccuracyDescriptor describes the relevant accuracy/complexity aspects of passcode user verification methods.
type CodeAccuracyDescriptor struct {
// The numeric system base (radix) of the code, e.g. 10 in the case of decimal digits.
Base uint16 `json:"base"`
// The minimum number of digits of the given base required for that code, e.g. 4 in the case of 4 digits.
MinLength uint16 `json:"minLength"`
// Maximum number of false attempts before the authenticator will block this method (at least for some time). 0 means it will never block.
MaxRetries uint16 `json:"maxRetries"`
// Enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar).
// 0 means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded.
// All alternative user verification methods MUST be specified appropriately in the Metadata in userVerificationDetails.
BlockSlowdown uint16 `json:"blockSlowdown"`
}
// The BiometricAccuracyDescriptor describes relevant accuracy/complexity aspects in the case of a biometric user verification method.
type BiometricAccuracyDescriptor struct {
// The false rejection rate [ISO19795-1] for a single template, i.e. the percentage of verification transactions with truthful claims of identity that are incorrectly denied.
SelfAttestedFRR int64 `json:"selfAttestedFRR "`
// The false acceptance rate [ISO19795-1] for a single template, i.e. the percentage of verification transactions with wrongful claims of identity that are incorrectly confirmed.
SelfAttestedFAR int64 `json:"selfAttestedFAR "`
// Maximum number of alternative templates from different fingers allowed.
MaxTemplates uint16 `json:"maxTemplates"`
// Maximum number of false attempts before the authenticator will block this method (at least for some time). 0 means it will never block.
MaxRetries uint16 `json:"maxRetries"`
// Enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar).
// 0 means that this user verification method will be blocked either permanently or until an alternative user verification method succeeded.
// All alternative user verification methods MUST be specified appropriately in the metadata in userVerificationDetails.
BlockSlowdown uint16 `json:"blockSlowdown"`
}
// The PatternAccuracyDescriptor describes relevant accuracy/complexity aspects in the case that a pattern is used as the user verification method.
type PatternAccuracyDescriptor struct {
// Number of possible patterns (having the minimum length) out of which exactly one would be the right one, i.e. 1/probability in the case of equal distribution.
MinComplexity uint32 `json:"minComplexity"`
// Maximum number of false attempts before the authenticator will block authentication using this method (at least temporarily). 0 means it will never block.
MaxRetries uint16 `json:"maxRetries"`
// Enforced minimum number of seconds wait time after blocking (due to forced reboot or similar mechanism).
// 0 means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded.
// All alternative user verification methods MUST be specified appropriately in the metadata under userVerificationDetails.
BlockSlowdown uint16 `json:"blockSlowdown"`
}
// VerificationMethodDescriptor - A descriptor for a specific base user verification method as implemented by the authenticator.
type VerificationMethodDescriptor struct {
// a single USER_VERIFY constant (see [FIDORegistry]), not a bit flag combination. This value MUST be non-zero.
UserVerification uint32 `json:"userVerification"`
// May optionally be used in the case of method USER_VERIFY_PASSCODE.
CaDesc CodeAccuracyDescriptor `json:"caDesc"`
// May optionally be used in the case of method USER_VERIFY_FINGERPRINT, USER_VERIFY_VOICEPRINT, USER_VERIFY_FACEPRINT, USER_VERIFY_EYEPRINT, or USER_VERIFY_HANDPRINT.
BaDesc BiometricAccuracyDescriptor `json:"baDesc"`
// May optionally be used in case of method USER_VERIFY_PATTERN.
PaDesc PatternAccuracyDescriptor `json:"paDesc"`
}
// VerificationMethodANDCombinations MUST be non-empty. It is a list containing the base user verification methods which must be passed as part of a successful user verification.
type VerificationMethodANDCombinations struct {
//This list will contain only a single entry if using a single user verification method is sufficient.
// If this list contains multiple entries, then all of the listed user verification methods MUST be passed as part of the user verification process.
VerificationMethodAndCombinations []VerificationMethodDescriptor `json:"verificationMethodANDCombinations"`
}
// The rgbPaletteEntry is an RGB three-sample tuple palette entry
type rgbPaletteEntry struct {
// Red channel sample value
R uint16 `json:"r"`
// Green channel sample value
G uint16 `json:"g"`
// Blue channel sample value
B uint16 `json:"b"`
}
// The DisplayPNGCharacteristicsDescriptor describes a PNG image characteristics as defined in the PNG [PNG] spec for IHDR (image header) and PLTE (palette table)
type DisplayPNGCharacteristicsDescriptor struct {
// image width
Width uint32 `json:"width"`
// image height
Height uint32 `json:"height"`
// Bit depth - bits per sample or per palette index.
BitDepth byte `json:"bitDepth"`
// Color type defines the PNG image type.
ColorType byte `json:"colorType"`
// Compression method used to compress the image data.
Compression byte `json:"compression"`
// Filter method is the preprocessing method applied to the image data before compression.
Filter byte `json:"filter"`
// Interlace method is the transmission order of the image data.
Interlace byte `json:"interlace"`
// 1 to 256 palette entries
Plte []rgbPaletteEntry `json:"plte"`
}
// EcdaaTrustAnchor - In the case of ECDAA attestation, the ECDAA-Issuer's trust anchor MUST be specified in this field.
type EcdaaTrustAnchor struct {
// base64url encoding of the result of ECPoint2ToB of the ECPoint2 X
X string `json:"x"`
// base64url encoding of the result of ECPoint2ToB of the ECPoint2 Y
Y string `json:"y"`
// base64url encoding of the result of BigNumberToB(c)
C string `json:"c"`
// base64url encoding of the result of BigNumberToB(sx)
SX string `json:"sx"`
// base64url encoding of the result of BigNumberToB(sy)
SY string `json:"sy"`
// Name of the Barreto-Naehrig elliptic curve for G1. "BN_P256", "BN_P638", "BN_ISOP256", and "BN_ISOP512" are supported.
G1Curve string `json:"G1Curve"`
}
// ExtensionDescriptor - This descriptor contains an extension supported by the authenticator.
type ExtensionDescriptor struct {
// Identifies the extension.
ID string `json:"id"`
// The TAG of the extension if this was assigned. TAGs are assigned to extensions if they could appear in an assertion.
Tag uint16 `json:"tag"`
// Contains arbitrary data further describing the extension and/or data needed to correctly process the extension.
Data string `json:"data"`
// Indicates whether unknown extensions must be ignored (false) or must lead to an error (true) when the extension is to be processed by the FIDO Server, FIDO Client, ASM, or FIDO Authenticator.
FailIfUnknown bool `json:"fail_if_unknown"`
}
// MetadataStatement - Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information contained in the authoritative statement is used in several other places.
type MetadataStatement struct {
// The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement.
LegalHeader string `json:"legalHeader"`
// The Authenticator Attestation ID.
Aaid string `json:"aaid"`
// The Authenticator Attestation GUID.
AaGUID string `json:"aaguid"`
// A list of the attestation certificate public key identifiers encoded as hex string.
AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"`
// A human-readable, short description of the authenticator, in English.
Description string `json:"description"`
// A list of human-readable short descriptions of the authenticator in different languages.
AlternativeDescriptions map[string]string `json:"alternativeDescriptions"`
// Earliest (i.e. lowest) trustworthy authenticatorVersion meeting the requirements specified in this metadata statement.
AuthenticatorVersion uint16 `json:"authenticatorVersion"`
// The FIDO protocol family. The values "uaf", "u2f", and "fido2" are supported.
ProtocolFamily string `json:"protocolFamily"`
// The FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator.
Upv []Version `json:"upv"`
// The assertion scheme supported by the authenticator.
AssertionScheme string `json:"assertionScheme"`
// The preferred authentication algorithm supported by the authenticator.
AuthenticationAlgorithm uint16 `json:"authenticationAlgorithm"`
// The list of authentication algorithms supported by the authenticator.
AuthenticationAlgorithms []uint16 `json:"authenticationAlgorithms"`
// The preferred public key format used by the authenticator during registration operations.
PublicKeyAlgAndEncoding uint16 `json:"publicKeyAlgAndEncoding"`
// The list of public key formats supported by the authenticator during registration operations.
PublicKeyAlgAndEncodings []uint16 `json:"publicKeyAlgAndEncodings"`
// The supported attestation type(s).
AttestationTypes []uint16 `json:"attestationTypes"`
// A list of alternative VerificationMethodANDCombinations.
UserVerificationDetails [][]VerificationMethodDescriptor `json:"userVerificationDetails"`
// A 16-bit number representing the bit fields defined by the KEY_PROTECTION constants in the FIDO Registry of Predefined Values
KeyProtection uint16 `json:"keyProtection"`
// This entry is set to true or it is ommitted, if the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions.
// This entry is set to false, if the authenticator doesn't restrict the Uauth key to only sign valid FIDO signature assertions.
IsKeyRestricted bool `json:"isKeyRestricted"`
// This entry is set to true or it is ommitted, if Uauth key usage always requires a fresh user verification
// This entry is set to false, if the Uauth key can be used without requiring a fresh user verification, e.g. without any additional user interaction, if the user was verified a (potentially configurable) caching time ago.
IsFreshUserVerificationRequired bool `json:"isFreshUserVerificationRequired"`
// A 16-bit number representing the bit fields defined by the MATCHER_PROTECTION constants in the FIDO Registry of Predefined Values
MatcherProtection uint16 `json:"matcherProtection"`
// The authenticator's overall claimed cryptographic strength in bits (sometimes also called security strength or security level).
CryptoStrength uint16 `json:"cryptoStrength"`
// Description of the particular operating environment that is used for the Authenticator.
OperatingEnv string `json:"operatingEnv"`
// A 32-bit number representing the bit fields defined by the ATTACHMENT_HINT constants in the FIDO Registry of Predefined Values
AttachmentHint uint32 `json:"attachmentHint"`
// Indicates if the authenticator is designed to be used only as a second factor, i.e. requiring some other authentication method as a first factor (e.g. username+password).
IsSecondFactorOnly bool `json:"isSecondFactorOnly"`
// A 16-bit number representing a combination of the bit flags defined by the TRANSACTION_CONFIRMATION_DISPLAY constants in the FIDO Registry of Predefined Values
TcDisplay uint16 `json:"tcDisplay"`
// Supported MIME content type [RFC2049] for the transaction confirmation display, such as text/plain or image/png.
TcDisplayContentType string `json:"tcDisplayContentType"`
// A list of alternative DisplayPNGCharacteristicsDescriptor. Each of these entries is one alternative of supported image characteristics for displaying a PNG image.
TcDisplayPNGCharacteristics []DisplayPNGCharacteristicsDescriptor `json:"tcDisplayPNGCharacteristics"`
// Each element of this array represents a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model.
// Multiple certificates might be used for different batches of the same model.
// The array does not represent a certificate chain, but only the trust anchor of that chain.
// A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself.
AttestationRootCertificates []string `json:"attestationRootCertificates"`
// A list of trust anchors used for ECDAA attestation. This entry MUST be present if and only if attestationType includes ATTESTATION_ECDAA.
EcdaaTrustAnchors []EcdaaTrustAnchor `json:"ecdaaTrustAnchors"`
// A data: url [RFC2397] encoded PNG [PNG] icon for the Authenticator.
Icon string `json:"icon"`
// List of extensions supported by the authenticator.
SupportedExtensions []ExtensionDescriptor `json:"supportedExtensions"`
}
// MDSGetEndpointsRequest is the request sent to the conformance metadata getEndpoints endpoint
type MDSGetEndpointsRequest struct {
// The URL of the local server endpoint, e.g. https://webauthn.io/
Endpoint string `json:"endpoint"`
}
// MDSGetEndpointsResponse is the response received from a conformance metadata getEndpoints request
type MDSGetEndpointsResponse struct {
// The status of the response
Status string `json:"status"`
// An array of urls, each pointing to a MetadataTOCPayload
Result []string `json:"result"`
}
// ProcessMDSTOC processes a FIDO metadata table of contents object per §3.1.8, steps 1 through 5
// FIDO Authenticator Metadata Service
// https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-service-v2.0-rd-20180702.html#metadata-toc-object-processing-rules
func ProcessMDSTOC(url string, suffix string, c http.Client) (MetadataTOCPayload, string, error) {
var tocAlg string
var payload MetadataTOCPayload
// 1. The FIDO Server MUST be able to download the latest metadata TOC object from the well-known URL, when appropriate.
body, err := downloadBytes(url+suffix, c)
if err != nil {
return payload, tocAlg, err
}
// Steps 2 - 4 done in unmarshalMDSTOC. Caller is responsible for step 5.
return unmarshalMDSTOC(body, c)
}
func unmarshalMDSTOC(body []byte, c http.Client) (MetadataTOCPayload, string, error) {
var tocAlg string
var payload MetadataTOCPayload
token, err := jwt.Parse(string(body), func(token *jwt.Token) (interface{}, error) {
// 2. If the x5u attribute is present in the JWT Header, then
if _, ok := token.Header["x5u"].([]interface{}); ok {
// never seen an x5u here, although it is in the spec
return nil, errors.New("x5u encountered in header of metadata TOC payload")
}
var chain []interface{}
// 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute.
if x5c, ok := token.Header["x5c"].([]interface{}); !ok {
// If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain.
root, err := getMetdataTOCSigningTrustAnchor(c)
if nil != err {
return nil, err
}
chain[0] = root
} else {
chain = x5c
}
// The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor
valid, err := validateChain(chain, c)
if !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))))
// base64 decode the certificate into the buffer
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
// parse the certificate from the buffer
cert, err := x509.ParseCertificate(o[:n])
if 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
})
if err != nil {
return payload, tocAlg, err
}
tocAlg = token.Header["alg"].(string)
err = mapstructure.Decode(token.Claims, &payload)
return payload, tocAlg, err
}
func getMetdataTOCSigningTrustAnchor(c http.Client) ([]byte, error) {
rooturl := ""
if Conformance {
rooturl = "https://fidoalliance.co.nz/mds/pki/MDSROOT.crt"
} else {
rooturl = "https://mds.fidoalliance.org/Root.cer"
}
return downloadBytes(rooturl, c)
}
func validateChain(chain []interface{}, c http.Client) (bool, error) {
root, err := getMetdataTOCSigningTrustAnchor(c)
if err != nil {
return false, err
}
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(root)
if !ok {
return false, err
}
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 {
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
}
// GetMetadataStatement iterates through a list of payload entries within a FIDO metadata table of contents object per §3.1.8, step 6
// FIDO Authenticator Metadata Service
// https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-service-v2.0-rd-20180702.html#metadata-toc-object-processing-rules
func GetMetadataStatement(entry MetadataTOCPayloadEntry, suffix string, alg string, c http.Client) (MetadataStatement, error) {
var statement MetadataStatement
// 1. Ignore the entry if the AAID, AAGUID or attestationCertificateKeyIdentifiers is not relevant to the relying party (e.g. not acceptable by any policy)
// Caller is responsible for determining if entry is relevant.
// 2. Download the metadata statement from the URL specified by the field url.
body, err := downloadBytes(entry.URL+suffix, c)
if err != nil {
return statement, err
}
// 3. Check whether the status report of the authenticator model has changed compared to the cached entry by looking at the fields timeOfLastStatusChange and statusReport.
// Caller is responsible for cache
// step 4 done in unmarshalMetadataStatement, caller is responsible for step 5
return unmarshalMetadataStatement(body, entry.Hash)
}
func unmarshalMetadataStatement(body []byte, hash string) (MetadataStatement, error) {
// 4. Compute the hash value of the metadata statement downloaded from the URL and verify the hash value to the hash specified in the field hash of the metadata TOC object.
var statement MetadataStatement
entryHash, err := base64.URLEncoding.DecodeString(hash)
if err != nil {
entryHash, err = base64.RawURLEncoding.DecodeString(hash)
}
if err != nil {
return statement, err
}
// TODO: Get hasher based on MDS TOC alg instead of assuming SHA256
hasher := crypto.SHA256.New()
_, _ = hasher.Write(body)
hashed := hasher.Sum(nil)
// Ignore the downloaded metadata statement if the hash value doesn't match.
if !bytes.Equal(hashed, entryHash) {
return statement, errHashValueMismatch
}
// Extract the metadata statement from base64 encoded form
n := base64.URLEncoding.DecodedLen(len(body))
out := make([]byte, n)
m, err := base64.URLEncoding.Decode(out, body)
if err != nil {
return statement, err
}
// Unmarshal the metadata statement into a MetadataStatement structure and return it to caller
err = json.Unmarshal(out[:m], &statement)
return statement, err
}
func downloadBytes(url string, c http.Client) ([]byte, error) {
res, err := c.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
return body, err
}
type MetadataError 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"`
}
var (
errHashValueMismatch = &MetadataError{
Type: "hash_mismatch",
Details: "Hash value mismatch between entry.Hash and downloaded bytes",
}
errIntermediateCertRevoked = &MetadataError{
Type: "intermediate_revoked",
Details: "Intermediate certificate is on issuers revocation list",
}
errLeafCertRevoked = &MetadataError{
Type: "leaf_revoked",
Details: "Leaf certificate is on issuers revocation list",
}
errCRLUnavailable = &MetadataError{
Type: "crl_unavailable",
Details: "Certificate revocation list is unavailable",
}
)
func (err *MetadataError) Error() string {
return err.Details
}

@ -0,0 +1,155 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
// The raw response returned to us from an authenticator when we request a
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
// The parsed CredentialAssertionResponse that has been marshalled into a format
// that allows us to verify the client and authenticator data inside the response
type ParsedCredentialAssertionData struct {
ParsedPublicKeyCredential
Response ParsedAssertionResponse
Raw CredentialAssertionResponse
}
// The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
// ParsedAssertionResponse
type AuthenticatorAssertionResponse struct {
AuthenticatorResponse
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
Signature URLEncodedBase64 `json:"signature"`
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
}
// Parsed form of AuthenticatorAssertionResponse
type ParsedAssertionResponse struct {
CollectedClientData CollectedClientData
AuthenticatorData AuthenticatorData
Signature []byte
UserHandle []byte
}
// Parse the credential request response into a format that is either required by the specification
// or makes the assertion verification steps easier to complete. This takes an http.Request that contains
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
// manageable structures
func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) {
if response == nil || response.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
return ParseCredentialRequestResponseBody(response.Body)
}
// Parse the credential request response into a format that is either required by the specification
// or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
// manageable structures
func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) {
var car CredentialAssertionResponse
err := json.NewDecoder(body).Decode(&car)
if err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion")
}
if car.ID == "" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
}
_, err = base64.RawURLEncoding.DecodeString(car.ID)
if err != nil {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
}
if car.Type != "public-key" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
}
var par ParsedCredentialAssertionData
par.ID, par.RawID, par.Type, par.ClientExtensionResults = car.ID, car.RawID, car.Type, car.ClientExtensionResults
par.Raw = car
par.Response.Signature = car.AssertionResponse.Signature
par.Response.UserHandle = car.AssertionResponse.UserHandle
// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// We don't call it cData but this is Step 5 in the spec.
err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData)
if err != nil {
return nil, err
}
err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData)
if err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
}
return &par, nil
}
// Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
// (https://www.w3.org/TR/webauthn/#verifying-assertion) and return an error if there
// is a failure during each step.
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, relyingPartyOrigin, 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."
// 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, relyingPartyOrigin)
if validError != nil {
return validError
}
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
var appIDHash [32]byte
if appID != "" {
appIDHash = sha256.Sum256([]byte(appID))
}
// Handle steps 11 through 14, verifying the authenticator data.
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser)
if validError != nil {
return ErrAuthData.WithInfo(validError.Error())
}
// allowedUserCredentialIDs := session.AllowedCredentialIDs
// Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON)
// Step 16. Using the credential public key looked up in step 3, verify that sig is
// a valid signature over the binary concatenation of authData and hash.
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...)
var (
key interface{}
err error
)
if appID == "" {
key, err = webauthncose.ParsePublicKey(credentialBytes)
} else {
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
}
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v\n", err))
}
return nil
}

@ -0,0 +1,156 @@
package protocol
import (
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/fxamacker/cbor/v2"
)
// From §5.2.1 (https://www.w3.org/TR/webauthn/#authenticatorattestationresponse)
// "The authenticator's response to a client’s request for the creation
// of a new public key credential. It contains information about the new credential
// that can be used to identify it for later use, and metadata that can be used by
// the WebAuthn Relying Party to assess the characteristics of the credential
// during registration."
// The initial unpacked 'response' object received by the relying party. This
// contains the clientDataJSON object, which will be marshalled into
// CollectedClientData, and the 'attestationObject', which contains
// information about the authenticator, and the newly minted
// public key credential. The information in both objects are used
// to verify the authenticity of the ceremony and new credential
type AuthenticatorAttestationResponse struct {
// The byte slice of clientDataJSON, which becomes CollectedClientData
AuthenticatorResponse
// 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
// attestation object contains both authenticator data and an attestation
// statement. The former contains the AAGUID, a unique credential ID, and
// the credential public key. The contents of the attestation statement are
// determined by the attestation statement format used by the authenticator.
// It also contains any additional information that the Relying Party's server
// 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"`
}
// The parsed out version of AuthenticatorAttestationResponse.
type ParsedAttestationResponse struct {
CollectedClientData CollectedClientData
AttestationObject AttestationObject
}
// From §6.4. Authenticators MUST also provide some form of attestation. The basic requirement is that the
// authenticator can produce, for each credential public key, an attestation statement verifiable by the
// WebAuthn Relying Party. Typically, this attestation statement contains a signature by an attestation
// private key over the attested credential public key and a challenge, as well as a certificate or similar
// data providing provenance information for the attestation public key, enabling the Relying Party to make
// a trust decision. However, if an attestation key pair is not available, then the authenticator MUST
// perform self attestation of the credential public key with the corresponding credential private key.
// All this information is returned by authenticators any time a new public key credential is generated, in
// the overall form of an attestation object. (https://www.w3.org/TR/webauthn/#attestation-object)
//
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"`
}
type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error)
var attestationRegistry = make(map[string]attestationFormatValidationHandler)
// Using one of the locally registered attestation formats, handle validating the attestation
// data provided by the authenticator (and in some cases its manufacturer)
func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) {
attestationRegistry[format] = handler
}
// Parse the values returned in the authenticator response and perform attestation verification
// Step 8. This returns a fully decoded struct with the data put into a format that can be
// used to verify the user and credential that was created
func (ccr *AuthenticatorAttestationResponse) Parse() (*ParsedAttestationResponse, error) {
var p ParsedAttestationResponse
err := json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData)
if err != nil {
return nil, ErrParsingData.WithInfo(err.Error())
}
err = cbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject)
if err != nil {
return nil, ErrParsingData.WithInfo(err.Error())
}
// 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.
err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData)
if err != nil {
return nil, fmt.Errorf("error decoding auth data: %v", err)
}
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() {
return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag")
}
return &p, nil
}
// Verify - Perform Steps 9 through 14 of registration verification, delegating Steps
func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool) error {
// 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
// Begin Step 9. Verify that the rpIdHash in authData is
// the SHA-256 hash of the RP ID expected by the RP.
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
// Handle Steps 9 through 12
authDataVerificationError := attestationObject.AuthData.Verify(rpIDHash[:], nil, verificationRequired)
if authDataVerificationError != nil {
return authDataVerificationError
}
// 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 {
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))
}
// 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, _, err := formatHandler(*attestationObject, clientDataHash)
if err != nil {
return err.(*Error).WithInfo(attestationType)
}
return nil
}

@ -0,0 +1,227 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
var androidAttestationKey = "android-key"
func init() {
RegisterAttestationFormat(androidAttestationKey, verifyAndroidKeyFormat)
}
// From §8.4. https://www.w3.org/TR/webauthn/#android-key-attestation
// The android-key attestation statement looks like:
// $$attStmtType //= (
// fmt: "android-key",
// attStmt: androidStmtFormat
// )
// androidStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, 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)
if !present {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
if !present {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}
// If x5c is not present, return an error
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
}
// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
// using the public key in the first certificate in x5c with the algorithm specified in alg.
attCertBytes, valid := x5c[0].([]byte)
if !valid {
return androidAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(att.RawAuthData, clientDataHash...)
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig)
if err != nil {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", 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 androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
e := pubKey.(webauthncose.EC2PublicKeyData)
valid, err = e.Verify(signatureData, sig)
if err != nil || valid != true {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
// attCert.Extensions
var attExtBytes []byte
for _, ext := range attCert.Extensions {
if ext.Id.Equal([]int{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
}
// As noted in §8.4.1 (https://w3c.github.io/webauthn/#key-attstn-cert-requirements) the Android Key Attestation attestation certificate's
// android key attestation certificate extension data is identified by the OID "1.3.6.1.4.1.11129.2.1.17".
decoded := keyDescription{}
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
if err != nil {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions")
}
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
if 0 != bytes.Compare(decoded.AttestationChallenge, clientDataHash) {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
}
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
if nil != decoded.SoftwareEnforced.AllApplications || nil != decoded.TeeEnforced.AllApplications {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
}
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. (which == 0)
if KM_ORIGIN_GENERATED != decoded.SoftwareEnforced.Origin || KM_ORIGIN_GENERATED != decoded.TeeEnforced.Origin {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
}
// The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. (which == 2)
if !contains(decoded.SoftwareEnforced.Purpose, KM_PURPOSE_SIGN) && !contains(decoded.TeeEnforced.Purpose, KM_PURPOSE_SIGN) {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
}
return androidAttestationKey, x5c, err
}
func contains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
type keyDescription struct {
AttestationVersion int
AttestationSecurityLevel asn1.Enumerated
KeymasterVersion int
KeymasterSecurityLevel asn1.Enumerated
AttestationChallenge []byte
UniqueID []byte
SoftwareEnforced authorizationList
TeeEnforced authorizationList
}
type authorizationList struct {
Purpose []int `asn1:"tag:1,explicit,set,optional"`
Algorithm int `asn1:"tag:2,explicit,optional"`
KeySize int `asn1:"tag:3,explicit,optional"`
Digest []int `asn1:"tag:5,explicit,set,optional"`
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"`
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"`
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"`
CreationDateTime int `asn1:"tag:701,explicit,optional"`
Origin int `asn1:"tag:702,explicit,optional"`
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
OsVersion int `asn1:"tag:705,explicit,optional"`
OsPatchLevel int `asn1:"tag:706,explicit,optional"`
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"`
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"`
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"`
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"`
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"`
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"`
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"`
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"`
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"`
VendorPatchLevel int `asn1:"tag:718,explicit,optional"`
BootPatchLevel int `asn1:"tag:719,explicit,optional"`
}
type rootOfTrust struct {
verifiedBootKey []byte
deviceLocked bool
verifiedBootState verifiedBootState
verifiedBootHash []byte
}
type verifiedBootState int
const (
Verified verifiedBootState = iota
SelfSigned
Unverified
Failed
)
/**
* The origin of a key (or pair), i.e. where it was generated. Note that KM_TAG_ORIGIN can be found
* in either the hardware-enforced or software-enforced list for a key, indicating whether the key
* is hardware or software-based. Specifically, a key with KM_ORIGIN_GENERATED in the
* hardware-enforced list is guaranteed never to have existed outide the secure hardware.
*/
type KM_KEY_ORIGIN int
const (
KM_ORIGIN_GENERATED = iota /* Generated in keymaster. Should not exist outside the TEE. */
KM_ORIGIN_DERIVED /* Derived inside keymaster. Likely exists off-device. */
KM_ORIGIN_IMPORTED /* Imported into keymaster. Existed as cleartext in Android. */
KM_ORIGIN_UNKNOWN /* Keymaster did not record origin. This value can only be seen on
* keys in a keymaster0 implementation. The keymaster0 adapter uses
* this value to document the fact that it is unkown whether the key
* was generated inside or imported into keymaster. */
)
/**
* Possible purposes of a key (or pair).
*/
type KM_PURPOSE int
const (
KM_PURPOSE_ENCRYPT = iota /* Usable with RSA, EC and AES keys. */
KM_PURPOSE_DECRYPT /* Usable with RSA, EC and AES keys. */
KM_PURPOSE_SIGN /* Usable with RSA, EC and HMAC keys. */
KM_PURPOSE_VERIFY /* Usable with RSA, EC and HMAC keys. */
KM_PURPOSE_DERIVE_KEY /* Usable with EC keys. */
KM_PURPOSE_WRAP /* Usable with wrapped keys. */
)

@ -0,0 +1,104 @@
package protocol
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"fmt"
"math/big"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
var appleAttestationKey = "apple"
func init() {
RegisterAttestationFormat(appleAttestationKey, verifyAppleKeyFormat)
}
// From §8.8. https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation
// The apple attestation statement looks like:
// $$attStmtType //= (
// fmt: "apple",
// attStmt: appleStmtFormat
// )
// appleStmtFormat = {
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
func verifyAppleKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, 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 !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
}
credCertBytes, valid := x5c[0].([]byte)
if !valid {
return appleAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
credCert, err := x509.ParseCertificate(credCertBytes)
if err != nil {
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
nonceToHash := append(att.RawAuthData, clientDataHash...)
// Step 3. Perform SHA-256 hash of nonceToHash to produce nonce.
nonce := sha256.Sum256(nonceToHash)
// Step 4. Verify that nonce equals the value of the extension with OID 1.2.840.113635.100.8.2 in credCert.
var attExtBytes []byte
for _, ext := range credCert.Extensions {
if ext.Id.Equal([]int{1, 2, 840, 113635, 100, 8, 2}) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.2.840.113635.100.8.2")
}
decoded := AppleAnonymousAttestation{}
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
if err != nil {
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions")
}
if !bytes.Equal(decoded.Nonce, nonce[:]) || err != nil {
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Attestation certificate does not contain expected nonce")
}
// Step 5. Verify that the credential public key equals the Subject Public Key of credCert.
// TODO: Probably move this part to webauthncose.go
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
if err != nil {
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
credPK := pubKey.(webauthncose.EC2PublicKeyData)
subjectPK := credCert.PublicKey.(*ecdsa.PublicKey)
credPKInfo := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: big.NewInt(0).SetBytes(credPK.XCoord),
Y: big.NewInt(0).SetBytes(credPK.YCoord),
}
if !credPKInfo.Equal(subjectPK) {
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Certificate public key does not match public key in authData")
}
// Step 6. If successful, return implementation-specific values representing attestation type Anonymization CA and attestation trust path x5c.
return appleAttestationKey, x5c, nil
}
// Apple has not yet publish schema for the extension(as of JULY 2021.)
type AppleAnonymousAttestation struct {
Nonce []byte `asn1:"tag:1,explicit"`
}

@ -0,0 +1,278 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"strings"
"time"
"github.com/duo-labs/webauthn/metadata"
uuid "github.com/satori/go.uuid"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
var packedAttestationKey = "packed"
func init() {
RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat)
}
// From §8.2. https://www.w3.org/TR/webauthn/#packed-attestation
// The packed attestation statement looks like:
// packedStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
// } OR
// {
// alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
// sig: bytes,
// ecdaaKeyId: bytes
// } OR
// {
// alg: COSEAlgorithmIdentifier
// sig: bytes,
// }
func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, 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)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if x509present {
// Handle Basic Attestation steps for the x509 Certificate
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c)
}
// 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)
if ecdaaKeyPresent {
// Handle ECDAA Attestation steps for the x509 Certificate
return handleECDAAAttesation(sig, clientDataHash, ecdaaKeyID)
}
// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
return handleSelfAttestation(alg, att.AuthData.AttData.CredentialPublicKey, att.RawAuthData, clientDataHash, sig)
}
// Handle the attestation steps laid out in
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, 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.
attestationType := "Packed (Basic)"
for _, c := range x5c {
cb, cv := c.([]byte)
if !cv {
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
ct, err := x509.ParseCertificate(cb)
if err != nil {
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
if ct.NotBefore.After(time.Now()) || ct.NotAfter.Before(time.Now()) {
return attestationType, x5c, ErrAttestationFormat.WithDetails("Cert in chain not time valid")
}
}
attCertBytes, valid := x5c[0].([]byte)
if !valid {
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(authData, clientDataHash...)
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, signature)
if err != nil {
return attestationType, x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
// §8.2.1 can be found here https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
// Step 2.2.1 (from §8.2.1) Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
if attCert.Version != 3 {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate is incorrect version")
}
// Step 2.2.2 (from §8.2.1) Subject field MUST be set to:
// Subject-C
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)
// TODO: Find a good, useable, country code library. For now, check stringy-ness
subjectString := strings.Join(attCert.Subject.Country, "")
if subjectString == "" {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid")
}
// Subject-O
// Legal name of the Authenticator vendor (UTF8String)
subjectString = strings.Join(attCert.Subject.Organization, "")
if subjectString == "" {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid")
}
// Subject-OU
// Literal string “Authenticator Attestation” (UTF8String)
subjectString = strings.Join(attCert.Subject.OrganizationalUnit, " ")
if subjectString != "Authenticator Attestation" {
// TODO: Implement a return error when I'm more certain this is general practice
}
// Subject-CN
// A UTF8String of the vendor’s choosing
subjectString = attCert.Subject.CommonName
if subjectString == "" {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set")
}
// TODO: And then what
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
idFido := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
var foundAAGUID []byte
for _, extension := range attCert.Extensions {
if extension.Id.Equal(idFido) {
if extension.Critical {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
}
foundAAGUID = extension.Value
}
}
// We validate the AAGUID as mentioned above
// This is not well defined in§8.2.1 but mentioned in step 2.3: we validate the AAGUID if it is present within the certificate
// and make sure it matches the auth data AAGUID
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
if len(foundAAGUID) > 0 {
unMarshalledAAGUID := []byte{}
asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID)
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
}
}
uuid, err := uuid.FromBytes(aaguid)
if meta, ok := metadata.Metadata[uuid]; ok {
for _, s := range meta.StatusReports {
if metadata.IsUndesiredAuthenticatorStatus(metadata.AuthenticatorStatus(s.Status)) {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered")
}
}
if attCert.Subject.CommonName != attCert.Issuer.CommonName {
var hasBasicFull = false
for _, a := range meta.MetadataStatement.AttestationTypes {
if metadata.AuthenticatorAttestationType(a) == metadata.AuthenticatorAttestationType(metadata.BasicFull) {
hasBasicFull = true
}
}
if !hasBasicFull {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation with full attestation from authentictor that does not support full attestation")
}
}
} else {
if metadata.Conformance {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("AAGUID not found in metadata during conformance testing")
}
}
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
if attCert.IsCA {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate's Basic Constraints marked as CA")
}
// Note for 2.2.5 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL
// Distribution Point extension [RFC5280](https://www.w3.org/TR/webauthn/#biblio-rfc5280) are
// both OPTIONAL as the status of many attestation certificates is available through authenticator
// metadata services. See, for example, the FIDO Metadata Service
// [FIDOMetadataService] (https://www.w3.org/TR/webauthn/#biblio-fidometadataservice)
// Step 2.4 If successful, return attestation type Basic and attestation trust path x5c.
// We don't handle trust paths yet but we're done
return attestationType, x5c, nil
}
func handleECDAAAttesation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
}
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) {
attestationType := "Packed (Self)"
// §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.
verificationData := append(authData, clientDataHash...)
key, err := webauthncose.ParsePublicKey(pubKey)
if err != nil {
return attestationType, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v\n", err))
}
switch key.(type) {
case webauthncose.OKPPublicKeyData:
k := key.(webauthncose.OKPPublicKeyData)
err := verifyKeyAlgorithm(k.Algorithm, alg)
if err != nil {
return attestationType, nil, err
}
case webauthncose.EC2PublicKeyData:
k := key.(webauthncose.EC2PublicKeyData)
err := verifyKeyAlgorithm(k.Algorithm, alg)
if err != nil {
return attestationType, nil, err
}
case webauthncose.RSAPublicKeyData:
k := key.(webauthncose.RSAPublicKeyData)
err := verifyKeyAlgorithm(k.Algorithm, alg)
if err != nil {
return attestationType, nil, err
}
default:
return attestationType, nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
}
valid, err := webauthncose.VerifySignature(key, verificationData, signature)
if !valid && err == nil {
return attestationType, nil, ErrInvalidAttestation.WithDetails("Unabled to verify signature")
}
return attestationType, nil, err
}
func verifyKeyAlgorithm(keyAlgorithm, attestedAlgorithm int64) error {
if keyAlgorithm != attestedAlgorithm {
return ErrInvalidAttestation.WithDetails("Public key algorithm does not equal att statement algorithm")
}
return nil
}

@ -0,0 +1,143 @@
package protocol
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
"github.com/duo-labs/webauthn/metadata"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/mitchellh/mapstructure"
)
var safetyNetAttestationKey = "android-safetynet"
func init() {
RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
}
type SafetyNetResponse struct {
Nonce string `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
// Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
// §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
// When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
// statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
// the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
// some statements about the health of the platform and the identity of the calling application. This attestation does not
// provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
// authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// The syntax of an Android Attestation statement is defined as follows:
// $$attStmtType //= (
// fmt: "android-safetynet",
// attStmt: safetynetStmtFormat
// )
// safetynetStmtFormat = {
// ver: text,
// response: bytes
// }
// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// We have done this
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
version, present := att.AttStatement["ver"].(string)
if !present {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
}
if version == "" {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
}
// TODO: provide user the ability to designate their supported versions
response, present := att.AttStatement["response"].([]byte)
if !present {
return safetyNetAttestationKey, 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{})
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
cert, err := x509.ParseCertificate(o[:n])
return cert.PublicKey, err
})
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// marshall the JWT payload into the safetynet response json
var safetyNetResponse SafetyNetResponse
err = mapstructure.Decode(token.Claims, &safetyNetResponse)
if err != nil {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", 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
// of authenticatorData and clientDataHash.
nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
}
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
certChain := token.Header["x5c"].([]interface{})
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
attestationCert, err := x509.ParseCertificate(l[:n])
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", 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 safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
if !safetyNetResponse.CtsProfileMatch {
return safetyNetAttestationKey, 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)
t := time.Unix(safetyNetResponse.TimestampMs/1000, 0)
if t.After(now) {
// zero tolerance for post-dated timestamps
return "Basic attestation with SafetyNet", 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 "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg)
}
}
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
// trust path attestationCert.
return "Basic attestation with SafetyNet", nil, nil
}

@ -0,0 +1,349 @@
package protocol
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"strings"
"github.com/duo-labs/webauthn/protocol/webauthncose"
"github.com/duo-labs/webauthn/protocol/googletpm"
)
var tpmAttestationKey = "tpm"
func init() {
RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat)
googletpm.UseTPM20LengthPrefixSize()
}
func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, 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)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving ver value")
}
if ver != "2.0" {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
}
alg, present := att.AttStatement["alg"].(int64)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return tpmAttestationKey, nil, ErrNotImplemented
}
_, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
if ecdaaKeyPresent {
return tpmAttestationKey, nil, ErrNotImplemented
}
sigBytes, present := att.AttStatement["sig"].([]byte)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}
certInfoBytes, present := att.AttStatement["certInfo"].([]byte)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving certInfo value")
}
pubAreaBytes, present := att.AttStatement["pubArea"].([]byte)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving pubArea value")
}
// Verify that the public key specified by the parameters and unique fields of pubArea
// is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
pubArea, err := googletpm.DecodePublic(pubAreaBytes)
if err != nil {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement")
}
key, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
switch key.(type) {
case webauthncose.EC2PublicKeyData:
e := key.(webauthncose.EC2PublicKeyData)
if pubArea.ECCParameters.CurveID != googletpm.EllipticCurve(e.Curve) ||
0 != pubArea.ECCParameters.Point.X.Cmp(new(big.Int).SetBytes(e.XCoord)) ||
0 != pubArea.ECCParameters.Point.Y.Cmp(new(big.Int).SetBytes(e.YCoord)) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
case webauthncose.RSAPublicKeyData:
r := key.(webauthncose.RSAPublicKeyData)
mod := new(big.Int).SetBytes(r.Modulus)
exp := uint32(r.Exponent[0]) + uint32(r.Exponent[1])<<8 + uint32(r.Exponent[2])<<16
if 0 != pubArea.RSAParameters.Modulus.Cmp(mod) ||
pubArea.RSAParameters.Exponent != exp {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
default:
return "", nil, ErrUnsupportedKey
}
// Concatenate authenticatorData and clientDataHash to form attToBeSigned
attToBeSigned := append(att.RawAuthData, clientDataHash...)
// Validate that certInfo is valid:
certInfo, err := googletpm.DecodeAttestationData(certInfoBytes)
// 1/4 Verify that magic is set to TPM_GENERATED_VALUE.
if certInfo.Magic != 0xff544347 {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Magic is not set to TPM_GENERATED_VALUE")
}
// 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
if certInfo.Type != googletpm.TagAttestCertify {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Type is not set to TPM_ST_ATTEST_CERTIFY")
}
// 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.Write(attToBeSigned)
if 0 != bytes.Compare(certInfo.ExtraData, h.Sum(nil)) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
}
// 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
// [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
// as computed using the algorithm in the nameAlg field of pubArea
// using the procedure specified in [TPMv2-Part1] section 16.
f, err = certInfo.AttestedCertifyInfo.Name.Digest.Alg.HashConstructor()
h = f()
h.Write(pubAreaBytes)
if 0 != bytes.Compare(h.Sum(nil), certInfo.AttestedCertifyInfo.Name.Digest.Value) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea")
}
// Note that the remaining fields in the "Standard Attestation Structure"
// [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion
// are ignored. These fields MAY be used as an input to risk engines.
// If x5c is present, this indicates that the attestation type is not ECDAA.
if x509present {
// In this case:
// Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
aikCertBytes, valid := x5c[0].([]byte)
if !valid {
return tpmAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
aikCert, err := x509.ParseCertificate(aikCertBytes)
if err != nil {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
}
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = aikCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), certInfoBytes, sigBytes)
if err != nil {
return tpmAttestationKey, 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
// 1/6 Version MUST be set to 3.
if aikCert.Version != 3 {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
}
// 2/6 Subject field MUST be set to empty.
if aikCert.Subject.String() != "" {
return tpmAttestationKey, 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
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 17}) {
manufacturer, model, version, err = parseSANExtension(ext.Value)
}
}
if manufacturer == "" || model == "" || version == "" {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
}
if false == isValidTPMManufacturer(manufacturer) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid TPM manufacturer")
}
// 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
var 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 tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3")
}
ekuValid = true
}
}
if false == ekuValid {
return tpmAttestationKey, 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 tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
} else if len(rest) != 0 {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
}
}
}
if constraints.IsCA != false {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
}
// 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.
}
return tpmAttestationKey, x5c, err
}
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
// RFC 5280, 4.2.1.6
// SubjectAltName ::= GeneralNames
//
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
//
// GeneralName ::= CHOICE {
// otherName [0] OtherName,
// rfc822Name [1] IA5String,
// dNSName [2] IA5String,
// x400Address [3] ORAddress,
// directoryName [4] Name,
// ediPartyName [5] EDIPartyName,
// uniformResourceIdentifier [6] IA5String,
// iPAddress [7] OCTET STRING,
// registeredID [8] OBJECT IDENTIFIER }
var seq asn1.RawValue
rest, err := asn1.Unmarshal(extension, &seq)
if err != nil {
return err
} else if len(rest) != 0 {
return errors.New("x509: trailing data after X.509 extension")
}
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
return asn1.StructuralError{Msg: "bad SAN sequence"}
}
rest = seq.Bytes
for len(rest) > 0 {
var v asn1.RawValue
rest, err = asn1.Unmarshal(rest, &v)
if err != nil {
return err
}
if err := callback(v.Tag, v.Bytes); err != nil {
return err
}
}
return nil
}
const (
nameTypeDN = 4
)
var (
tcgKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
tcgAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
tcgAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
tcgAtTpmVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
)
func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) {
err = forEachSAN(value, func(tag int, data []byte) error {
switch tag {
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 {
continue
}
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
})
return
}
var tpmManufacturers = []struct {
id string
name string
code string
}{
{"414D4400", "AMD", "AMD"},
{"41544D4C", "Atmel", "ATML"},
{"4252434D", "Broadcom", "BRCM"},
{"49424d00", "IBM", "IBM"},
{"49465800", "Infineon", "IFX"},
{"494E5443", "Intel", "INTC"},
{"4C454E00", "Lenovo", "LEN"},
{"4E534D20", "National Semiconductor", "NSM"},
{"4E545A00", "Nationz", "NTZ"},
{"4E544300", "Nuvoton Technology", "NTC"},
{"51434F4D", "Qualcomm", "QCOM"},
{"534D5343", "SMSC", "SMSC"},
{"53544D20", "ST Microelectronics", "STM"},
{"534D534E", "Samsung", "SMSN"},
{"534E5300", "Sinosun", "SNS"},
{"54584E00", "Texas Instruments", "TXN"},
{"57454300", "Winbond", "WEC"},
{"524F4343", "Fuzhouk Rockchip", "ROCC"},
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
}
func isValidTPMManufacturer(id string) bool {
for _, m := range tpmManufacturers {
if m.id == id {
return true
}
}
return false
}

@ -0,0 +1,135 @@
package protocol
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"github.com/duo-labs/webauthn/protocol/webauthncose"
"github.com/fxamacker/cbor/v2"
)
var u2fAttestationKey = "fido-u2f"
func init() {
RegisterAttestationFormat(u2fAttestationKey, 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) {
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 u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00")
}
// Signing procedure step - If the credential public key of the given credential is not of
// algorithm -7 ("ES256"), stop and return an error.
key := webauthncose.EC2PublicKeyData{}
cbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &key)
if webauthncose.COSEAlgorithmIdentifier(key.PublicKeyData.Algorithm) != webauthncose.AlgES256 {
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("Non-ES256 Public Key algorithm used")
}
// U2F 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.
// The Format/syntax is
// u2fStmtFormat = {
// x5c: [ attestnCert: bytes ],
// sig: bytes
// }
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
x5c, present := att.AttStatement["x5c"].([]interface{})
if !present {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data")
}
// 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)
if !present {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing sig data")
}
// U2F Step 2. (1) Check that x5c has exactly one element and let attCert be that element. (2) Let certificate public
// key be the public key conveyed by attCert. (3) If certificate public key is not an Elliptic Curve (EC) public
// key over the P-256 curve, terminate this algorithm and return an appropriate error.
// Step 2.1
if len(x5c) > 1 {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Received more than one element in x5c values")
}
// Note: Packed Attestation, FIDO U2F Attestation, and Assertion Signatures support ASN.1,but it is recommended
// that any new attestation formats defined not use ASN.1 encodings, but instead represent signatures as equivalent
// fixed-length byte arrays without internal structure, using the same representations as used by COSE signatures
// as defined in RFC8152 (https://www.w3.org/TR/webauthn/#biblio-rfc8152)
// and RFC8230 (https://www.w3.org/TR/webauthn/#biblio-rfc8230).
// Step 2.2
asn1Bytes, decoded := x5c[0].([]byte)
if !decoded {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error decoding ASN.1 data from x5c")
}
attCert, err := x509.ParseCertificate(asn1Bytes)
if err != nil {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate")
}
// Step 2.3
if attCert.PublicKeyAlgorithm != x509.ECDSA && attCert.PublicKey.(*ecdsa.PublicKey).Curve != elliptic.P256() {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate is in invalid format")
}
// Step 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey
// from authenticatorData.attestedCredentialData.
rpIdHash := att.AuthData.RPIDHash
credentialID := att.AuthData.AttData.CredentialID
// credentialPublicKey handled earlier
// Step 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of RFC8152 [https://www.w3.org/TR/webauthn/#biblio-rfc8152])
// to Raw ANSI X9.62 public key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key
// Representation Formats of FIDO-Registry [https://www.w3.org/TR/webauthn/#biblio-fido-registry]).
// Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey, and confirm
// its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm and
// return an appropriate error.
// Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey, and confirm
// its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm and
// return an appropriate error.
if len(key.XCoord) > 32 || len(key.YCoord) > 32 {
return u2fAttestationKey, nil, ErrAttestation.WithDetails("X or Y Coordinate for key is invalid length")
}
// Let publicKeyU2F be the concatenation 0x04 || x || y.
publicKeyU2F := bytes.NewBuffer([]byte{0x04})
publicKeyU2F.Write(key.XCoord)
publicKeyU2F.Write(key.YCoord)
// Step 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
// (see §4.3 of FIDO-U2F-Message-Formats [https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]).
verificationData := bytes.NewBuffer([]byte{0x00})
verificationData.Write(rpIdHash)
verificationData.Write(clientDataHash)
verificationData.Write(credentialID)
verificationData.Write(publicKeyU2F.Bytes())
// Step 6. Verify the sig using verificationData and certificate public key per SEC1[https://www.w3.org/TR/webauthn/#biblio-sec1].
sigErr := attCert.CheckSignature(x509.ECDSAWithSHA256, verificationData.Bytes(), signature)
if sigErr != nil {
return u2fAttestationKey, nil, sigErr
}
// Step 7. If successful, return attestation type Basic with the attestation trust path set to x5c.
return "Fido U2F Basic", x5c, sigErr
}

@ -0,0 +1,256 @@
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/fxamacker/cbor/v2"
)
var minAuthDataLength = 37
// Authenticators respond to Relying Party requests by returning an object derived from the
// AuthenticatorResponse interface. See §5.2. Authenticator Responses
// https://www.w3.org/TR/webauthn/#iface-authenticatorresponse
type AuthenticatorResponse struct {
// From the spec https://www.w3.org/TR/webauthn/#dom-authenticatorresponse-clientdatajson
// This attribute contains a JSON serialization of the client data passed to the authenticator
// by the client in its call to either create() or get().
ClientDataJSON URLEncodedBase64 `json:"clientDataJSON"`
}
// AuthenticatorData From §6.1 of the spec.
// The authenticator data structure encodes contextual bindings made by the authenticator. These bindings
// are controlled by the authenticator itself, and derive their trust from the WebAuthn Relying Party's
// assessment of the security properties of the authenticator. In one extreme case, the authenticator
// may be embedded in the client, and its bindings may be no more trustworthy than the client data.
// At the other extreme, the authenticator may be a discrete entity with high-security hardware and
// software, connected to the client over a secure channel. In both cases, the Relying Party receives
// the authenticator data in the same format, and uses its knowledge of the authenticator to make
// trust decisions.
//
// The authenticator data, at least during attestation, contains the Public Key that the RP stores
// and will associate with the user attempting to register.
type AuthenticatorData struct {
RPIDHash []byte `json:"rpid"`
Flags AuthenticatorFlags `json:"flags"`
Counter uint32 `json:"sign_count"`
AttData AttestedCredentialData `json:"att_data"`
ExtData []byte `json:"ext_data"`
}
type AttestedCredentialData struct {
AAGUID []byte `json:"aaguid"`
CredentialID []byte `json:"credential_id"`
// The raw credential public key bytes received from the attestation data
CredentialPublicKey []byte `json:"public_key"`
}
// AuthenticatorAttachment https://www.w3.org/TR/webauthn/#platform-attachment
type AuthenticatorAttachment string
const (
// Platform - A platform authenticator is attached using a client device-specific transport, called
// platform attachment, and is usually not removable from the client device. A public key credential
// bound to a platform authenticator is called a platform credential.
Platform AuthenticatorAttachment = "platform"
// CrossPlatform A roaming authenticator is attached using cross-platform transports, called
// cross-platform attachment. Authenticators of this class are removable from, and can "roam"
// among, client devices. A public key credential bound to a roaming authenticator is called a
// roaming credential.
CrossPlatform AuthenticatorAttachment = "cross-platform"
)
// Authenticators may implement various transports for communicating with clients. This enumeration defines
// hints as to how clients might communicate with a particular authenticator in order to obtain an assertion
// for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to
// how an authenticator may be reached. A Relying Party may obtain a list of transports hints from some
// attestation statement formats or via some out-of-band mechanism; it is outside the scope of this
// specification to define that mechanism.
// See §5.10.4. Authenticator Transport https://www.w3.org/TR/webauthn/#transport
type AuthenticatorTransport string
const (
// USB The authenticator should transport information over USB
USB AuthenticatorTransport = "usb"
// NFC The authenticator should transport information over Near Field Communication Protocol
NFC AuthenticatorTransport = "nfc"
// BLE The authenticator should transport information over Bluetooth
BLE AuthenticatorTransport = "ble"
// Internal the client should use an internal source like a TPM or SE
Internal AuthenticatorTransport = "internal"
)
// A WebAuthn Relying Party may require user verification for some of its operations but not for others,
// and may use this type to express its needs.
// See §5.10.6. User Verification Requirement Enumeration https://www.w3.org/TR/webauthn/#userVerificationRequirement
type UserVerificationRequirement string
const (
// VerificationRequired User verification is required to create/release a credential
VerificationRequired UserVerificationRequirement = "required"
// VerificationPreferred User verification is preferred to create/release a credential
VerificationPreferred UserVerificationRequirement = "preferred" // This is the default
// VerificationDiscouraged The authenticator should not verify the user for the credential
VerificationDiscouraged UserVerificationRequirement = "discouraged"
)
// AuthenticatorFlags A byte of information returned during during ceremonies in the
// authenticatorData that contains bits that give us information about the
// whether the user was present and/or verified during authentication, and whether
// there is attestation or extension data present. Bit 0 is the least significant bit.
type AuthenticatorFlags byte
// The bits that do not have flags are reserved for future use.
const (
// FlagUserPresent Bit 00000001 in the byte sequence. Tells us if user is present
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP
_ // Reserved
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
// by the authenticator using a biometric or PIN
FlagUserVerified // Referred to as UV
_ // Reserved
_ // Reserved
_ // Reserved
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
// the authenticator added attested credential data.
FlagAttestedCredentialData // Referred to as AT
// FlagHasExtension Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions.
FlagHasExtensions // Referred to as ED
)
// UserPresent returns if the UP flag was set
func (flag AuthenticatorFlags) UserPresent() bool {
return (flag & FlagUserPresent) == FlagUserPresent
}
// UserVerified returns if the UV flag was set
func (flag AuthenticatorFlags) UserVerified() bool {
return (flag & FlagUserVerified) == FlagUserVerified
}
// HasAttestedCredentialData returns if the AT flag was set
func (flag AuthenticatorFlags) HasAttestedCredentialData() bool {
return (flag & FlagAttestedCredentialData) == FlagAttestedCredentialData
}
// HasExtensions returns if the ED flag was set
func (flag AuthenticatorFlags) HasExtensions() bool {
return (flag & FlagHasExtensions) == FlagHasExtensions
}
// Unmarshal will take the raw Authenticator Data and marshalls it into AuthenticatorData for further validation.
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be
// devices with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
// The authenticator data structure is a byte array of 37 bytes or more, and is laid out in this table:
// https://www.w3.org/TR/webauthn/#table-authData
func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) error {
if minAuthDataLength > len(rawAuthData) {
err := ErrBadRequest.WithDetails("Authenticator data length too short")
info := fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes\n", minAuthDataLength, len(rawAuthData))
return err.WithInfo(info)
}
a.RPIDHash = rawAuthData[:32]
a.Flags = AuthenticatorFlags(rawAuthData[32])
a.Counter = binary.BigEndian.Uint32(rawAuthData[33:37])
remaining := len(rawAuthData) - minAuthDataLength
if a.Flags.HasAttestedCredentialData() {
if len(rawAuthData) > minAuthDataLength {
a.unmarshalAttestedData(rawAuthData)
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey)
remaining = remaining - attDataLen
} else {
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing")
}
} else {
if !a.Flags.HasExtensions() && len(rawAuthData) != 37 {
return ErrBadRequest.WithDetails("Attested credential flag not set")
}
}
if a.Flags.HasExtensions() {
if remaining != 0 {
a.ExtData = rawAuthData[len(rawAuthData)-remaining:]
remaining -= len(a.ExtData)
} else {
return ErrBadRequest.WithDetails("Extensions flag set but extensions data is missing")
}
}
if remaining != 0 {
return ErrBadRequest.WithDetails("Leftover bytes decoding AuthenticatorData")
}
return nil
}
// If Attestation Data is present, unmarshall that into the appropriate public key structure
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) {
a.AttData.AAGUID = rawAuthData[37:53]
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
a.AttData.CredentialPublicKey = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
}
// Unmarshall the credential's Public Key into CBOR encoding
func unmarshalCredentialPublicKey(keyBytes []byte) []byte {
var m interface{}
cbor.Unmarshal(keyBytes, &m)
rawBytes, _ := cbor.Marshal(m)
return rawBytes
}
// ResidentKeyRequired - Require that the key be private key resident to the client device
func ResidentKeyRequired() *bool {
required := true
return &required
}
// ResidentKeyUnrequired - Do not require that the private key be resident to the client device.
func ResidentKeyUnrequired() *bool {
required := false
return &required
}
// Verify on AuthenticatorData handles Steps 9 through 12 for Registration
// and Steps 11 through 14 for Assertion.
func (a *AuthenticatorData) Verify(rpIdHash, appIDHash []byte, userVerificationRequired bool) error {
// Registration Step 9 & Assertion Step 11
// Verify that the RP ID hash in authData is indeed the SHA-256
// hash of the RP ID expected by the RP.
if !bytes.Equal(a.RPIDHash[:], rpIdHash) && !bytes.Equal(a.RPIDHash[:], appIDHash) {
return ErrVerification.WithInfo(fmt.Sprintf("RP Hash mismatch. Expected %s and Received %s\n", a.RPIDHash, rpIdHash))
}
// Registration Step 10 & Assertion Step 12
// Verify that the User Present bit of the flags in authData is set.
if !a.Flags.UserPresent() {
return ErrVerification.WithInfo(fmt.Sprintln("User presence flag not set by authenticator"))
}
// Registration Step 11 & Assertion Step 13
// If user verification is required for this assertion, verify that
// the User Verified bit of the flags in authData is set.
if userVerificationRequired && !a.Flags.UserVerified() {
return ErrVerification.WithInfo(fmt.Sprintln("User verification required but flag not set by authenticator"))
}
// Registration Step 12 & Assertion Step 14
// Verify that the values of the client extension outputs in clientExtensionResults
// and the authenticator extension outputs in the extensions in authData are as
// expected, considering the client extension input values that were given as the
// extensions option in the create() call. In particular, any extension identifier
// values in the clientExtensionResults and the extensions in authData MUST be also be
// present as extension identifier values in the extensions member of options, i.e., no
// extensions are present that were not requested. In the general case, the meaning
// of "are as expected" is specific to the Relying Party and which extensions are in use.
// This is not yet fully implemented by the spec or by browsers
return nil
}

@ -0,0 +1,41 @@
package protocol
import (
"bytes"
"encoding/base64"
"reflect"
)
// URLEncodedBase64 represents a byte slice holding URL-encoded base64 data.
// When fields of this type are unmarshaled from JSON, the data is base64
// decoded into a byte slice.
type URLEncodedBase64 []byte
// UnmarshalJSON base64 decodes a URL-encoded value, storing the result in the
// provided byte slice.
func (dest *URLEncodedBase64) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
return nil
}
// Trim the leading spaces
data = bytes.Trim(data, "\"")
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
n, err := base64.RawURLEncoding.Decode(out, data)
if err != nil {
return err
}
v := reflect.ValueOf(dest).Elem()
v.SetBytes(out[:n])
return nil
}
// MarshalJSON base64 encodes a non URL-encoded value, storing the result in the
// provided byte slice.
func (data URLEncodedBase64) MarshalJSON() ([]byte, error) {
if data == nil {
return []byte("null"), nil
}
return []byte(`"` + base64.RawURLEncoding.EncodeToString(data) + `"`), nil
}

@ -0,0 +1,27 @@
package protocol
import (
"crypto/rand"
"encoding/base64"
)
// ChallengeLength - Length of bytes to generate for a challenge
const ChallengeLength = 32
// Challenge that should be signed and returned by the authenticator
type Challenge URLEncodedBase64
// Create a new challenge to be sent to the authenticator. The spec recommends using
// at least 16 bytes with 100 bits of entropy. We use 32 bytes.
func CreateChallenge() (Challenge, error) {
challenge := make([]byte, ChallengeLength)
_, err := rand.Read(challenge)
if err != nil {
return nil, err
}
return challenge, nil
}
func (c Challenge) String() string {
return base64.RawURLEncoding.EncodeToString(c)
}

@ -0,0 +1,112 @@
package protocol
import (
"fmt"
"net/url"
"strings"
)
// CollectedClientData represents the contextual bindings of both the WebAuthn Relying Party
// and the client. It is a key-value mapping whose keys are strings. Values can be any type
// that has a valid encoding in JSON. Its structure is defined by the following Web IDL.
// https://www.w3.org/TR/webauthn/#sec-client-data
type CollectedClientData struct {
// Type the string "webauthn.create" when creating new credentials,
// and "webauthn.get" when getting an assertion from an existing credential. The
// purpose of this member is to prevent certain types of signature confusion attacks
//(where an attacker substitutes one legitimate signature for another).
Type CeremonyType `json:"type"`
Challenge string `json:"challenge"`
Origin string `json:"origin"`
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"`
// Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner
Hint string `json:"new_keys_may_be_added_here,omitempty"`
}
type CeremonyType string
const (
CreateCeremony CeremonyType = "webauthn.create"
AssertCeremony CeremonyType = "webauthn.get"
)
type TokenBinding struct {
Status TokenBindingStatus `json:"status"`
ID string `json:"id,omitempty"`
}
type TokenBindingStatus string
const (
// Indicates token binding was used when communicating with the
// Relying Party. In this case, the id member MUST be present.
Present TokenBindingStatus = "present"
// Indicates token binding was used when communicating with the
// negotiated when communicating with the Relying Party.
Supported TokenBindingStatus = "supported"
// Indicates token binding not supported
// when communicating with the Relying Party.
NotSupported TokenBindingStatus = "not-supported"
)
// Returns the origin per the HTML spec: (scheme)://(host)[:(port)]
func FullyQualifiedOrigin(u *url.URL) string {
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
// Handles steps 3 through 6 of verfying the registering client data of a
// new credential and steps 7 through 10 of verifying an authentication assertion
// See https://www.w3.org/TR/webauthn/#registering-a-new-credential
// and https://www.w3.org/TR/webauthn/#verifying-assertion
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, relyingPartyOrigin string) error {
// Registration Step 3. Verify that the value of C.type is webauthn.create.
// Assertion Step 7. Verify that the value of C.type is the string webauthn.get.
if c.Type != ceremony {
err := ErrVerification.WithDetails("Error validating ceremony type")
err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", ceremony, c.Type))
return err
}
// Registration Step 4. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the create() call.
// Assertion Step 8. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the PublicKeyCredentialRequestOptions
// passed to the get() call.
challenge := c.Challenge
if 0 != strings.Compare(storedChallenge, challenge) {
err := ErrVerification.WithDetails("Error validating challenge")
return err.WithInfo(fmt.Sprintf("Expected b Value: %#v\nReceived b: %#v\n", storedChallenge, challenge))
}
// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
// the Relying Party's origin.
clientDataOrigin, err := url.Parse(c.Origin)
if err != nil {
return ErrParsingData.WithDetails("Error decoding clientData origin as URL")
}
if !strings.EqualFold(FullyQualifiedOrigin(clientDataOrigin), relyingPartyOrigin) {
err := ErrVerification.WithDetails("Error validating origin")
return err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", relyingPartyOrigin, FullyQualifiedOrigin(clientDataOrigin)))
}
// Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status
// matches the state of Token Binding for the TLS connection over which the assertion was
// obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id
// matches the base64url encoding of the Token Binding ID for the connection.
if c.TokenBinding != nil {
if c.TokenBinding.Status == "" {
return ErrParsingData.WithDetails("Error decoding clientData, token binding present without status")
}
if c.TokenBinding.Status != Present && c.TokenBinding.Status != Supported && c.TokenBinding.Status != NotSupported {
return ErrParsingData.WithDetails("Error decoding clientData, token binding present with invalid status").WithInfo(fmt.Sprintf("Got: %s\n", c.TokenBinding.Status))
}
}
// Not yet fully implemented by the spec, browsers, and me.
return nil
}

@ -0,0 +1,216 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"net/http"
)
// The basic credential type that is inherited by WebAuthn's
// PublicKeyCredential type
// https://w3c.github.io/webappsec-credential-management/#credential
type Credential struct {
// ID is The credential’s identifier. The requirements for the
// identifier are distinct for each type of credential. It might
// represent a username for username/password tuples, for example.
ID string `json:"id"`
// Type is the value of the object’s interface object's [[type]] slot,
// which specifies the credential type represented by this object.
// This should be type "public-key" for Webauthn credentials.
Type string `json:"type"`
}
// The PublicKeyCredential interface inherits from Credential, and contains
// the attributes that are returned to the caller when a new credential
// is created, or a new assertion is requested.
type ParsedCredential struct {
ID string `cbor:"id"`
Type string `cbor:"type"`
}
type PublicKeyCredential struct {
Credential
RawID URLEncodedBase64 `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
}
type ParsedPublicKeyCredential struct {
ParsedCredential
RawID []byte `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
}
type CredentialCreationResponse struct {
PublicKeyCredential
AttestationResponse AuthenticatorAttestationResponse `json:"response"`
}
type ParsedCredentialCreationData struct {
ParsedPublicKeyCredential
Response ParsedAttestationResponse
Raw CredentialCreationResponse
}
func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialCreationData, error) {
if response == nil || response.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
return ParseCredentialCreationResponseBody(response.Body)
}
func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreationData, error) {
var ccr CredentialCreationResponse
err := json.NewDecoder(body).Decode(&ccr)
if err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error())
}
if ccr.ID == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID")
}
testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID)
if err != nil || !(len(testB64) > 0) {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("ID not base64.RawURLEncoded")
}
if ccr.PublicKeyCredential.Credential.Type == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type")
}
if ccr.PublicKeyCredential.Credential.Type != "public-key" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
}
var pcc ParsedCredentialCreationData
pcc.ID, pcc.RawID, pcc.Type = ccr.ID, ccr.RawID, ccr.Type
pcc.Raw = ccr
parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
if err != nil {
return nil, ErrParsingData.WithDetails("Error parsing attestation response")
}
pcc.Response = *parsedAttestationResponse
return &pcc, nil
}
// Verifies the Client and Attestation data as laid out by §7.1. Registering a new credential
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID, relyingPartyOrigin string) error {
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigin)
if verifyError != nil {
return verifyError
}
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
// 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. is handled while
// We do the above step while parsing and decoding the CredentialCreationResponse
// Handle steps 9 through 14 - This verifies the attestaion object and
verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser)
if verifyError != nil {
return verifyError
}
// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
// one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
// the future, however.
// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
// the set of acceptable trust anchors obtained in step 15.
// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
// attestation public key correctly chains up to an acceptable root certificate.
// TODO: We're not supporting trust anchors, self-attestation policy, or acceptable root certs yet
// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
// requested for a credential that is already registered to a different user, the Relying Party SHOULD
// fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting
// the older registration.
// TODO: We can't support this in the code's current form, the Relying Party would need to check for this
// against their database
// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
// register the new credential with the account that was denoted in the options.user passed to create(), by
// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
// appropriate for the Relying Party's system.
// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
// the Relying Party SHOULD fail the registration ceremony.
// TODO: Not implemented for the reasons mentioned under Step 16
return nil
}
// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
//
// 1. Check that the Session Data's AuthenticationExtensions has been provided and return a blank appid if it hasn't been.
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return a blank appid if it doesn't.
// 3. Check that the Credential AttestationType is `fido-u2f` and return a blank appid if it isn't.
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and return a blank appid if it doesn't.
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and return an error if it isn't.
// 6. Check that the appid output is true and return a blank appid if it isn't.
// 7. Check that the Session Data has an appid extension defined and return an error if it doesn't.
// 8. Check that the appid extension in Session Data is a string and return an error if it isn't.
// 9. Return the appid extension value from the Session Data.
func (ppkc ParsedPublicKeyCredential) GetAppID(authExt AuthenticationExtensions, credentialAttestationType string) (appID string, err error) {
var (
value, clientValue interface{}
enableAppID, ok bool
)
if authExt == nil {
return "", nil
}
if ppkc.ClientExtensionResults == nil {
return "", nil
}
// If the credential does not have the correct attestation type it is assumed to NOT be a fido-u2f credential.
// https://w3c.github.io/webauthn/#sctn-fido-u2f-attestation
if credentialAttestationType != "fido-u2f" {
return "", nil
}
if clientValue, ok = ppkc.ClientExtensionResults["appid"]; !ok {
return "", nil
}
if enableAppID, ok = clientValue.(bool); !ok {
return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type")
}
if !enableAppID {
return "", nil
}
if value, ok = authExt["appid"]; !ok {
return "", ErrBadRequest.WithDetails("Session Data does not have an appid but Client Output indicates it should be set")
}
if appID, ok = value.(string); !ok {
return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type")
}
return appID, nil
}

@ -0,0 +1,8 @@
// The protocol package contains data structures and validation functionality
// outlined in the Web Authnentication specification (https://www.w3.org/TR/webauthn).
// The data structures here attempt to conform as much as possible to their definitions,
// but some structs (like those that are used as part of validation steps) contain
// additional fields that help us unpack and validate the data we unmarshall.
// When implementing this library, most developers will primarily be using the API
// outlined in the webauthn package.
package protocol

@ -0,0 +1,48 @@
package protocol
// From §5.4.1 (https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity).
// PublicKeyCredentialEntity describes a user account, or a WebAuthn Relying Party,
// with which a public key credential is associated.
type CredentialEntity struct {
// A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents:
//
// When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the Relying Party,
// intended only for display. For example, "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех".
//
// When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a user account. It is
// intended only for display, i.e., aiding the user in determining the difference between user accounts with similar
// displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234".
Name string `json:"name"`
// A serialized URL which resolves to an image associated with the entity. For example,
// this could be a user’s avatar or a Relying Party's logo. This URL MUST be an a priori
// authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for
// an icon member’s value. Authenticators MAY ignore an icon member’s value if its length
// is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL,
// at the cost of needing more storage.
Icon string `json:"icon,omitempty"`
}
// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params).
// The PublicKeyCredentialRpEntity is used to supply additional
// Relying Party attributes when creating a new credential.
type RelyingPartyEntity struct {
CredentialEntity
// A unique identifier for the Relying Party entity, which sets the RP ID.
ID string `json:"id"`
}
// From §5.4.3 (https://www.w3.org/TR/webauthn/#sctn-user-credential-params).
// The PublicKeyCredentialUserEntity is used to supply additional
// user account attributes when creating a new credential.
type UserEntity struct {
CredentialEntity
// A human-palatable name for the user account, intended only for display.
// For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let
// the user choose this, and SHOULD NOT restrict the choice more than necessary.
DisplayName string `json:"displayName,omitempty"`
// ID is the user handle of the user account entity. To ensure secure operation,
// authentication and authorization decisions MUST be made on the basis of this id
// member, not the displayName nor name members. See Section 6.1 of
// [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266).
ID []byte `json:"id"`
}

@ -0,0 +1,85 @@
package protocol
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"`
}
var (
ErrBadRequest = &Error{
Type: "invalid_request",
Details: "Error reading the requst data",
}
ErrChallengeMismatch = &Error{
Type: "challenge_mismatch",
Details: "Stored challenge and received challenge do not match",
}
ErrParsingData = &Error{
Type: "parse_error",
Details: "Error parsing the authenticator response",
}
ErrAuthData = &Error{
Type: "auth_data",
Details: "Error verifying the authenticator data",
}
ErrVerification = &Error{
Type: "verification_error",
Details: "Error validating the authenticator response",
}
ErrAttestation = &Error{
Type: "attesation_error",
Details: "Error validating the attestation data provided",
}
ErrInvalidAttestation = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation data",
}
ErrAttestationFormat = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation format",
}
ErrAttestationCertificate = &Error{
Type: "invalid_certificate",
Details: "Invalid attestation certificate",
}
ErrAssertionSignature = &Error{
Type: "invalid_signature",
Details: "Assertion Signature against auth data and client hash is not valid",
}
ErrUnsupportedKey = &Error{
Type: "invalid_key_type",
Details: "Unsupported Public Key Type",
}
ErrUnsupportedAlgorithm = &Error{
Type: "unsupported_key_algorithm",
Details: "Unsupported public key algorithm",
}
ErrNotSpecImplemented = &Error{
Type: "spec_unimplemented",
Details: "This field is not yet supported by the WebAuthn spec",
}
ErrNotImplemented = &Error{
Type: "not_implemented",
Details: "This field is not yet supported by this library",
}
)
func (err *Error) Error() string {
return err.Details
}
func (passedError *Error) WithDetails(details string) *Error {
err := *passedError
err.Details = details
return &err
}
func (passedError *Error) WithInfo(info string) *Error {
err := *passedError
err.DevInfo = info
return &err
}

@ -0,0 +1,8 @@
package protocol
// Extensions are discussed in §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions).
// For a list of commonly supported extenstions, see §10. Defined Extensions
// (https://www.w3.org/TR/webauthn/#sctn-defined-extensions).
type AuthenticationExtensionsClientOutputs map[string]interface{}

@ -0,0 +1,282 @@
package googletpm
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
)
// DecodeAttestationData decode a TPMS_ATTEST message. No error is returned if
// the input has extra trailing data.
func DecodeAttestationData(in []byte) (*AttestationData, error) {
buf := bytes.NewBuffer(in)
var ad AttestationData
if err := UnpackBuf(buf, &ad.Magic, &ad.Type); err != nil {
return nil, fmt.Errorf("decoding Magic/Type: %v", err)
}
n, err := decodeName(buf)
if err != nil {
return nil, fmt.Errorf("decoding QualifiedSigner: %v", err)
}
ad.QualifiedSigner = *n
if err := UnpackBuf(buf, &ad.ExtraData, &ad.ClockInfo, &ad.FirmwareVersion); err != nil {
return nil, fmt.Errorf("decoding ExtraData/ClockInfo/FirmwareVersion: %v", err)
}
// The spec specifies several other types of attestation data. We only need
// parsing of Certify & Creation attestation data for now. If you need
// support for other attestation types, add them here.
switch ad.Type {
case TagAttestCertify:
if ad.AttestedCertifyInfo, err = decodeCertifyInfo(buf); err != nil {
return nil, fmt.Errorf("decoding AttestedCertifyInfo: %v", err)
}
case TagAttestCreation:
if ad.AttestedCreationInfo, err = decodeCreationInfo(buf); err != nil {
return nil, fmt.Errorf("decoding AttestedCreationInfo: %v", err)
}
case TagAttestQuote:
if ad.AttestedQuoteInfo, err = decodeQuoteInfo(buf); err != nil {
return nil, fmt.Errorf("decoding AttestedQuoteInfo: %v", err)
}
default:
return nil, fmt.Errorf("only Certify & Creation attestation structures are supported, got type 0x%x", ad.Type)
}
return &ad, nil
}
// AttestationData contains data attested by TPM commands (like Certify).
type AttestationData struct {
Magic uint32
Type Tag
QualifiedSigner Name
ExtraData []byte
ClockInfo ClockInfo
FirmwareVersion uint64
AttestedCertifyInfo *CertifyInfo
AttestedQuoteInfo *QuoteInfo
AttestedCreationInfo *CreationInfo
}
// Tag is a command tag.
type Tag uint16
type Name struct {
Handle *Handle
Digest *HashValue
}
// A Handle is a reference to a TPM object.
type Handle uint32
type HashValue struct {
Alg Algorithm
Value []byte
}
// ClockInfo contains TPM state info included in AttestationData.
type ClockInfo struct {
Clock uint64
ResetCount uint32
RestartCount uint32
Safe byte
}
// CertifyInfo contains Certify-specific data for TPMS_ATTEST.
type CertifyInfo struct {
Name Name
QualifiedName Name
}
// QuoteInfo represents a TPMS_QUOTE_INFO structure.
type QuoteInfo struct {
PCRSelection PCRSelection
PCRDigest []byte
}
// PCRSelection contains a slice of PCR indexes and a hash algorithm used in
// them.
type PCRSelection struct {
Hash Algorithm
PCRs []int
}
// CreationInfo contains Creation-specific data for TPMS_ATTEST.
type CreationInfo struct {
Name Name
// Most TPM2B_Digest structures contain a TPMU_HA structure
// and get parsed to HashValue. This is never the case for the
// digest in TPMS_CREATION_INFO.
OpaqueDigest []byte
}
func decodeName(in *bytes.Buffer) (*Name, error) {
var nameBuf []byte
if err := UnpackBuf(in, &nameBuf); err != nil {
return nil, err
}
name := new(Name)
switch len(nameBuf) {
case 0:
// No name is present.
case 4:
name.Handle = new(Handle)
if err := UnpackBuf(bytes.NewBuffer(nameBuf), name.Handle); err != nil {
return nil, fmt.Errorf("decoding Handle: %v", err)
}
default:
var err error
name.Digest, err = decodeHashValue(bytes.NewBuffer(nameBuf))
if err != nil {
return nil, fmt.Errorf("decoding Digest: %v", err)
}
}
return name, nil
}
func decodeHashValue(in *bytes.Buffer) (*HashValue, error) {
var hv HashValue
if err := UnpackBuf(in, &hv.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
hfn, ok := hashConstructors[hv.Alg]
if !ok {
return nil, fmt.Errorf("unsupported hash algorithm type 0x%x", hv.Alg)
}
hv.Value = make([]byte, hfn().Size())
if _, err := in.Read(hv.Value); err != nil {
return nil, fmt.Errorf("decoding Value: %v", err)
}
return &hv, nil
}
// HashConstructor returns a function that can be used to make a
// hash.Hash using the specified algorithm. An error is returned
// if the algorithm is not a hash algorithm.
func (a Algorithm) HashConstructor() (func() hash.Hash, error) {
c, ok := hashConstructors[a]
if !ok {
return nil, fmt.Errorf("algorithm not supported: 0x%x", a)
}
return c, nil
}
var hashConstructors = map[Algorithm]func() hash.Hash{
AlgSHA1: sha1.New,
AlgSHA256: sha256.New,
AlgSHA384: sha512.New384,
AlgSHA512: sha512.New,
}
// TPM Structure Tags. Tags are used to disambiguate structures, similar to Alg
// values: tag value defines what kind of data lives in a nested field.
const (
TagNull Tag = 0x8000
TagNoSessions Tag = 0x8001
TagSessions Tag = 0x8002
TagAttestCertify Tag = 0x8017
TagAttestQuote Tag = 0x8018
TagAttestCreation Tag = 0x801a
TagHashCheck Tag = 0x8024
)
func decodeCertifyInfo(in *bytes.Buffer) (*CertifyInfo, error) {
var ci CertifyInfo
n, err := decodeName(in)
if err != nil {
return nil, fmt.Errorf("decoding Name: %v", err)
}
ci.Name = *n
n, err = decodeName(in)
if err != nil {
return nil, fmt.Errorf("decoding QualifiedName: %v", err)
}
ci.QualifiedName = *n
return &ci, nil
}
func decodeCreationInfo(in *bytes.Buffer) (*CreationInfo, error) {
var ci CreationInfo
n, err := decodeName(in)
if err != nil {
return nil, fmt.Errorf("decoding Name: %v", err)
}
ci.Name = *n
if err := UnpackBuf(in, &ci.OpaqueDigest); err != nil {
return nil, fmt.Errorf("decoding Digest: %v", err)
}
return &ci, nil
}
func decodeQuoteInfo(in *bytes.Buffer) (*QuoteInfo, error) {
var out QuoteInfo
sel, err := decodeTPMLPCRSelection(in)
if err != nil {
return nil, fmt.Errorf("decoding PCRSelection: %v", err)
}
out.PCRSelection = sel
if err := UnpackBuf(in, &out.PCRDigest); err != nil {
return nil, fmt.Errorf("decoding PCRDigest: %v", err)
}
return &out, nil
}
func decodeTPMLPCRSelection(buf *bytes.Buffer) (PCRSelection, error) {
var count uint32
var sel PCRSelection
if err := UnpackBuf(buf, &count); err != nil {
return sel, err
}
switch count {
case 0:
sel.Hash = AlgUnknown
return sel, nil
case 1: // We only support decoding of a single PCRSelection.
default:
return sel, fmt.Errorf("decoding TPML_PCR_SELECTION list longer than 1 is not supported (got length %d)", count)
}
// See comment in encodeTPMLPCRSelection for details on this format.
var ts tpmsPCRSelection
if err := UnpackBuf(buf, &ts.Hash, &ts.Size); err != nil {
return sel, err
}
ts.PCRs = make([]byte, ts.Size)
if _, err := buf.Read(ts.PCRs); err != nil {
return sel, err
}
sel.Hash = ts.Hash
for i := 0; i < int(ts.Size); i++ {
for j := 0; j < 8; j++ {
set := ts.PCRs[i] & byte(1<<byte(j))
if set == 0 {
continue
}
sel.PCRs = append(sel.PCRs, 8*i+j)
}
}
return sel, nil
}
type tpmsPCRSelection struct {
Hash Algorithm
Size byte
PCRs RawBytes
}
// RawBytes is for Pack and RunCommand arguments that are already encoded.
// Compared to []byte, RawBytes will not be prepended with slice length during
// encoding.
type RawBytes []byte

@ -0,0 +1,152 @@
package googletpm
import (
"encoding/binary"
"errors"
"fmt"
"io"
"reflect"
)
// From github.com/google/go-tpm
// Portions of existing package conflicted with existing build environment
// and only needed very small amount of code for pubarea and certinfo structs
// so copied them out to this package
// Supported Algorithms.
const (
AlgUnknown Algorithm = 0x0000
AlgRSA Algorithm = 0x0001
AlgSHA1 Algorithm = 0x0004
AlgAES Algorithm = 0x0006
AlgKeyedHash Algorithm = 0x0008
AlgSHA256 Algorithm = 0x000B
AlgSHA384 Algorithm = 0x000C
AlgSHA512 Algorithm = 0x000D
AlgNull Algorithm = 0x0010
AlgRSASSA Algorithm = 0x0014
AlgRSAES Algorithm = 0x0015
AlgRSAPSS Algorithm = 0x0016
AlgOAEP Algorithm = 0x0017
AlgECDSA Algorithm = 0x0018
AlgECDH Algorithm = 0x0019
AlgECDAA Algorithm = 0x001A
AlgKDF2 Algorithm = 0x0021
AlgECC Algorithm = 0x0023
AlgCTR Algorithm = 0x0040
AlgOFB Algorithm = 0x0041
AlgCBC Algorithm = 0x0042
AlgCFB Algorithm = 0x0043
AlgECB Algorithm = 0x0044
)
// UnpackBuf recursively unpacks types from a reader just as encoding/binary
// does under binary.BigEndian, but with one difference: it unpacks a byte
// slice by first reading an integer with lengthPrefixSize bytes, then reading
// that many bytes. It assumes that incoming values are pointers to values so
// that, e.g., underlying slices can be resized as needed.
func UnpackBuf(buf io.Reader, elts ...interface{}) error {
for _, e := range elts {
v := reflect.ValueOf(e)
k := v.Kind()
if k != reflect.Ptr {
return fmt.Errorf("all values passed to Unpack must be pointers, got %v", k)
}
if v.IsNil() {
return errors.New("can't fill a nil pointer")
}
iv := reflect.Indirect(v)
switch iv.Kind() {
case reflect.Struct:
// Decompose the struct and copy over the values.
for i := 0; i < iv.NumField(); i++ {
if err := UnpackBuf(buf, iv.Field(i).Addr().Interface()); err != nil {
return err
}
}
case reflect.Slice:
var size int
_, isHandles := e.(*[]Handle)
switch {
// []Handle always uses 2-byte length, even with TPM 1.2.
case isHandles:
var tmpSize uint16
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
return err
}
size = int(tmpSize)
// TPM 2.0
case lengthPrefixSize == tpm20PrefixSize:
var tmpSize uint16
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
return err
}
size = int(tmpSize)
// TPM 1.2
case lengthPrefixSize == tpm12PrefixSize:
var tmpSize uint32
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
return err
}
size = int(tmpSize)
default:
return fmt.Errorf("lengthPrefixSize is %d, must be either 2 or 4", lengthPrefixSize)
}
// A zero size is used by the TPM to signal that certain elements
// are not present.
if size == 0 {
continue
}
// Make len(e) match size exactly.
switch b := e.(type) {
case *[]byte:
if len(*b) >= size {
*b = (*b)[:size]
} else {
*b = append(*b, make([]byte, size-len(*b))...)
}
case *[]Handle:
if len(*b) >= size {
*b = (*b)[:size]
} else {
*b = append(*b, make([]Handle, size-len(*b))...)
}
default:
return fmt.Errorf("can't fill pointer to %T, only []byte or []Handle slices", e)
}
if err := binary.Read(buf, binary.BigEndian, e); err != nil {
return err
}
default:
if err := binary.Read(buf, binary.BigEndian, e); err != nil {
return err
}
}
}
return nil
}
// lengthPrefixSize is the size in bytes of length prefix for byte slices.
//
// In TPM 1.2 this is 4 bytes.
// In TPM 2.0 this is 2 bytes.
var lengthPrefixSize int
const (
tpm12PrefixSize = 4
tpm20PrefixSize = 2
)
// UseTPM20LengthPrefixSize makes Pack/Unpack use TPM 2.0 encoding for byte
// arrays.
func UseTPM20LengthPrefixSize() {
lengthPrefixSize = tpm20PrefixSize
}

@ -0,0 +1,240 @@
package googletpm
import (
"bytes"
"fmt"
"math/big"
)
// DecodePublic decodes a TPMT_PUBLIC message. No error is returned if
// the input has extra trailing data.
func DecodePublic(buf []byte) (Public, error) {
in := bytes.NewBuffer(buf)
var pub Public
var err error
if err = UnpackBuf(in, &pub.Type, &pub.NameAlg, &pub.Attributes, &pub.AuthPolicy); err != nil {
return pub, fmt.Errorf("decoding TPMT_PUBLIC: %v", err)
}
switch pub.Type {
case AlgRSA:
pub.RSAParameters, err = decodeRSAParams(in)
case AlgECC:
pub.ECCParameters, err = decodeECCParams(in)
default:
err = fmt.Errorf("unsupported type in TPMT_PUBLIC: %v", pub.Type)
}
return pub, err
}
// Public contains the public area of an object.
type Public struct {
Type Algorithm
NameAlg Algorithm
Attributes KeyProp
AuthPolicy []byte
// If Type is AlgKeyedHash, then do not set these.
// Otherwise, only one of the Parameters fields should be set. When encoding/decoding,
// one will be picked based on Type.
RSAParameters *RSAParams
ECCParameters *ECCParams
}
// Algorithm represents a TPM_ALG_ID value.
type Algorithm uint16
// KeyProp is a bitmask used in Attributes field of key templates. Individual
// flags should be OR-ed to form a full mask.
type KeyProp uint32
// Key properties.
const (
FlagFixedTPM KeyProp = 0x00000002
FlagFixedParent KeyProp = 0x00000010
FlagSensitiveDataOrigin KeyProp = 0x00000020
FlagUserWithAuth KeyProp = 0x00000040
FlagAdminWithPolicy KeyProp = 0x00000080
FlagNoDA KeyProp = 0x00000400
FlagRestricted KeyProp = 0x00010000
FlagDecrypt KeyProp = 0x00020000
FlagSign KeyProp = 0x00040000
FlagSealDefault = FlagFixedTPM | FlagFixedParent
FlagSignerDefault = FlagSign | FlagRestricted | FlagFixedTPM |
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
FlagStorageDefault = FlagDecrypt | FlagRestricted | FlagFixedTPM |
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
)
func decodeRSAParams(in *bytes.Buffer) (*RSAParams, error) {
var params RSAParams
var err error
if params.Symmetric, err = decodeSymScheme(in); err != nil {
return nil, fmt.Errorf("decoding Symmetric: %v", err)
}
if params.Sign, err = decodeSigScheme(in); err != nil {
return nil, fmt.Errorf("decoding Sign: %v", err)
}
var modBytes []byte
if err := UnpackBuf(in, &params.KeyBits, &params.Exponent, &modBytes); err != nil {
return nil, fmt.Errorf("decoding KeyBits, Exponent, Modulus: %v", err)
}
if params.Exponent == 0 {
params.encodeDefaultExponentAsZero = true
params.Exponent = defaultRSAExponent
}
params.Modulus = new(big.Int).SetBytes(modBytes)
return &params, nil
}
const defaultRSAExponent = 1<<16 + 1
// RSAParams represents parameters of an RSA key pair.
//
// Symmetric and Sign may be nil, depending on key Attributes in Public.
//
// One of Modulus and ModulusRaw must always be non-nil. Modulus takes
// precedence. ModulusRaw is used for key templates where the field named
// "unique" must be a byte array of all zeroes.
type RSAParams struct {
Symmetric *SymScheme
Sign *SigScheme
KeyBits uint16
// The default Exponent (65537) has two representations; the
// 0 value, and the value 65537.
// If encodeDefaultExponentAsZero is set, an exponent of 65537
// will be encoded as zero. This is necessary to produce an identical
// encoded bitstream, so Name digest calculations will be correct.
encodeDefaultExponentAsZero bool
Exponent uint32
ModulusRaw []byte
Modulus *big.Int
}
// SymScheme represents a symmetric encryption scheme.
type SymScheme struct {
Alg Algorithm
KeyBits uint16
Mode Algorithm
} // SigScheme represents a signing scheme.
type SigScheme struct {
Alg Algorithm
Hash Algorithm
Count uint32
}
func decodeSigScheme(in *bytes.Buffer) (*SigScheme, error) {
var scheme SigScheme
if err := UnpackBuf(in, &scheme.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
if scheme.Alg == AlgNull {
return nil, nil
}
if err := UnpackBuf(in, &scheme.Hash); err != nil {
return nil, fmt.Errorf("decoding Hash: %v", err)
}
if scheme.Alg.UsesCount() {
if err := UnpackBuf(in, &scheme.Count); err != nil {
return nil, fmt.Errorf("decoding Count: %v", err)
}
}
return &scheme, nil
}
// UsesCount returns true if a signature algorithm uses count value.
func (a Algorithm) UsesCount() bool {
return a == AlgECDAA
}
func decodeKDFScheme(in *bytes.Buffer) (*KDFScheme, error) {
var scheme KDFScheme
if err := UnpackBuf(in, &scheme.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
if scheme.Alg == AlgNull {
return nil, nil
}
if err := UnpackBuf(in, &scheme.Hash); err != nil {
return nil, fmt.Errorf("decoding Hash: %v", err)
}
return &scheme, nil
}
func decodeSymScheme(in *bytes.Buffer) (*SymScheme, error) {
var scheme SymScheme
if err := UnpackBuf(in, &scheme.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
if scheme.Alg == AlgNull {
return nil, nil
}
if err := UnpackBuf(in, &scheme.KeyBits, &scheme.Mode); err != nil {
return nil, fmt.Errorf("decoding KeyBits, Mode: %v", err)
}
return &scheme, nil
}
func decodeECCParams(in *bytes.Buffer) (*ECCParams, error) {
var params ECCParams
var err error
if params.Symmetric, err = decodeSymScheme(in); err != nil {
return nil, fmt.Errorf("decoding Symmetric: %v", err)
}
if params.Sign, err = decodeSigScheme(in); err != nil {
return nil, fmt.Errorf("decoding Sign: %v", err)
}
if err := UnpackBuf(in, &params.CurveID); err != nil {
return nil, fmt.Errorf("decoding CurveID: %v", err)
}
if params.KDF, err = decodeKDFScheme(in); err != nil {
return nil, fmt.Errorf("decoding KDF: %v", err)
}
var x, y []byte
if err := UnpackBuf(in, &x, &y); err != nil {
return nil, fmt.Errorf("decoding Point: %v", err)
}
params.Point.X = new(big.Int).SetBytes(x)
params.Point.Y = new(big.Int).SetBytes(y)
return &params, nil
}
// ECCParams represents parameters of an ECC key pair.
//
// Symmetric, Sign and KDF may be nil, depending on key Attributes in Public.
type ECCParams struct {
Symmetric *SymScheme
Sign *SigScheme
CurveID EllipticCurve
KDF *KDFScheme
Point ECPoint
}
// EllipticCurve identifies specific EC curves.
type EllipticCurve uint16
// ECC curves supported by TPM 2.0 spec.
const (
CurveNISTP192 = EllipticCurve(iota + 1)
CurveNISTP224
CurveNISTP256
CurveNISTP384
CurveNISTP521
CurveBNP256 = EllipticCurve(iota + 10)
CurveBNP638
CurveSM2P256 = EllipticCurve(0x0020)
)
// ECPoint represents a ECC coordinates for a point.
type ECPoint struct {
X, Y *big.Int
}
// KDFScheme represents a KDF (Key Derivation Function) scheme.
type KDFScheme struct {
Alg Algorithm
Hash Algorithm
}

@ -0,0 +1,136 @@
package protocol
import (
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
type CredentialCreation struct {
Response PublicKeyCredentialCreationOptions `json:"publicKey"`
}
type CredentialAssertion struct {
Response PublicKeyCredentialRequestOptions `json:"publicKey"`
}
// In order to create a Credential via create(), the caller specifies a few parameters in a CredentialCreationOptions object.
// See §5.4. Options for Credential Creation https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions
type PublicKeyCredentialCreationOptions struct {
Challenge Challenge `json:"challenge"`
RelyingParty RelyingPartyEntity `json:"rp"`
User UserEntity `json:"user"`
Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"`
AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"`
Timeout int `json:"timeout,omitempty"`
CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"`
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
Attestation ConveyancePreference `json:"attestation,omitempty"`
}
// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion.
// Its challenge member MUST be present, while its other members are OPTIONAL.
// See §5.5. Options for Assertion Generation https://www.w3.org/TR/webauthn/#assertion-options
type PublicKeyCredentialRequestOptions struct {
Challenge Challenge `json:"challenge"`
Timeout int `json:"timeout,omitempty"`
RelyingPartyID string `json:"rpId,omitempty"`
AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"`
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` // Default is "preferred"
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
}
// This dictionary contains the attributes that are specified by a caller when referring to a public
// key credential as an input parameter to the create() or get() methods. It mirrors the fields of
// the PublicKeyCredential object returned by the latter methods.
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credential-dictionary
type CredentialDescriptor struct {
// The valid credential types.
Type CredentialType `json:"type"`
// CredentialID The ID of a credential to allow/disallow
CredentialID []byte `json:"id"`
// The authenticator transports that can be used
Transport []AuthenticatorTransport `json:"transports,omitempty"`
}
// CredentialParameter is the credential type and algorithm
// that the relying party wants the authenticator to create
type CredentialParameter struct {
Type CredentialType `json:"type"`
Algorithm webauthncose.COSEAlgorithmIdentifier `json:"alg"`
}
// This enumeration defines the valid credential types.
// It is an extension point; values can be added to it in the future, as
// more credential types are defined. The values of this enumeration are used
// for versioning the Authentication Assertion and attestation structures according
// to the type of the authenticator.
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credentialType
type CredentialType string
const (
// PublicKeyCredentialType - Currently one credential type is defined, namely "public-key".
PublicKeyCredentialType CredentialType = "public-key"
)
// AuthenticationExtensions - referred to as AuthenticationExtensionsClientInputs in the
// spec document, this member contains additional parameters requesting additional processing
// by the client and authenticator.
// This is currently under development
type AuthenticationExtensions map[string]interface{}
// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements
// regarding authenticator attributes. See §5.4.4. Authenticator Selection Criteria
// https://www.w3.org/TR/webauthn/#authenticatorSelection
type AuthenticatorSelection struct {
// AuthenticatorAttachment If this member is present, eligible authenticators are filtered to only
// authenticators attached with the specified AuthenticatorAttachment enum
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
// RequireResidentKey this member describes the Relying Party's requirements regarding resident
// credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident
// public key credential source when creating a public key credential.
RequireResidentKey *bool `json:"requireResidentKey,omitempty"`
// UserVerification This member describes the Relying Party's requirements regarding user verification for
// the create() operation. Eligible authenticators are filtered to only those capable of satisfying this
// requirement.
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
}
// WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding
// attestation conveyance during credential generation. See §5.4.6. https://www.w3.org/TR/webauthn/#attestation-convey
type ConveyancePreference string
const (
// The default value. This value indicates that the Relying Party is not interested in authenticator attestation. For example,
// in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to
// save a roundtrip to an Attestation CA.
PreferNoAttestation ConveyancePreference = "none"
// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation
// statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace
// the authenticator-generated attestation statements with attestation statements generated by an Anonymization
// CA, in order to protect the user’s privacy, or to assist Relying Parties with attestation verification in a
// heterogeneous ecosystem.
PreferIndirectAttestation ConveyancePreference = "indirect"
// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator.
PreferDirectAttestation ConveyancePreference = "direct"
)
func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte {
var allowedCredentialIDs = make([][]byte, len(a.AllowedCredentials))
for i, credential := range a.AllowedCredentials {
allowedCredentialIDs[i] = credential.CredentialID
}
return allowedCredentialIDs
}
type Extensions interface{}
type ServerResponse struct {
Status ServerResponseStatus `json:"status"`
Message string `json:"errorMessage"`
}
type ServerResponseStatus string
const (
StatusOk ServerResponseStatus = "ok"
StatusFailed ServerResponseStatus = "failed"
)

@ -0,0 +1,12 @@
// +build go1.13
package webauthncose
import (
"crypto/ed25519"
"crypto/x509"
)
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
return x509.MarshalPKIXPublicKey(pub)
}

@ -0,0 +1,38 @@
// +build !go1.13
package webauthncose
import (
"crypto/x509/pkix"
"encoding/asn1"
"golang.org/x/crypto/ed25519"
)
var oidSignatureEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}
type pkixPublicKey struct {
Algo pkix.AlgorithmIdentifier
BitString asn1.BitString
}
// marshalEd25519PublicKey is a backport of the functionality introduced in
// Go v1.13.
// Ref: https://golang.org/doc/go1.13#crypto/ed25519
// Ref: https://golang.org/doc/go1.13#crypto/x509
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
publicKeyBytes := pub
var publicKeyAlgorithm pkix.AlgorithmIdentifier
publicKeyAlgorithm.Algorithm = oidSignatureEd25519
pkix := pkixPublicKey{
Algo: publicKeyAlgorithm,
BitString: asn1.BitString{
Bytes: publicKeyBytes,
BitLength: 8 * len(publicKeyBytes),
},
}
ret, _ := asn1.Marshal(pkix)
return ret, nil
}

@ -0,0 +1,400 @@
package webauthncose
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"hash"
"math/big"
"github.com/fxamacker/cbor/v2"
"golang.org/x/crypto/ed25519"
)
// PublicKeyData The public key portion of a Relying Party-specific credential key pair, generated
// by an authenticator and returned to a Relying Party at registration time. We unpack this object
// using fxamacker's cbor library ("github.com/fxamacker/cbor/v2") which is why there are cbor tags
// included. The tag field values correspond to the IANA COSE keys that give their respective
// values.
// See §6.4.1.1 https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples for examples of this
// COSE data.
type PublicKeyData struct {
// Decode the results to int by default.
_struct bool `cbor:",keyasint" json:"public_key"`
// The type of key created. Should be OKP, EC2, or RSA.
KeyType int64 `cbor:"1,keyasint" json:"kty"`
// A COSEAlgorithmIdentifier for the algorithm used to derive the key signature.
Algorithm int64 `cbor:"3,keyasint" json:"alg"`
}
type EC2PublicKeyData struct {
PublicKeyData
// If the key type is EC2, the curve on which we derive the signature from.
Curve int64 `cbor:"-1,keyasint,omitempty" json:"crv"`
// A byte string 32 bytes in length that holds the x coordinate of the key.
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
// A byte string 32 bytes in length that holds the y coordinate of the key.
YCoord []byte `cbor:"-3,keyasint,omitempty" json:"y"`
}
type RSAPublicKeyData struct {
PublicKeyData
// Represents the modulus parameter for the RSA algorithm
Modulus []byte `cbor:"-1,keyasint,omitempty" json:"n"`
// Represents the exponent parameter for the RSA algorithm
Exponent []byte `cbor:"-2,keyasint,omitempty" json:"e"`
}
type OKPPublicKeyData struct {
PublicKeyData
Curve int64
// A byte string that holds the x coordinate of the key.
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
}
// Verify Octet Key Pair (OKP) Public Key Signature
func (k *OKPPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
var key ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(key, k.XCoord)
return ed25519.Verify(key, data, sig), nil
}
// Verify Elliptic Curce Public Key Signature
func (k *EC2PublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
var curve elliptic.Curve
switch COSEAlgorithmIdentifier(k.Algorithm) {
case AlgES512: // IANA COSE code for ECDSA w/ SHA-512
curve = elliptic.P521()
case AlgES384: // IANA COSE code for ECDSA w/ SHA-384
curve = elliptic.P384()
case AlgES256: // IANA COSE code for ECDSA w/ SHA-256
curve = elliptic.P256()
default:
return false, ErrUnsupportedAlgorithm
}
pubkey := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}
type ECDSASignature struct {
R, S *big.Int
}
e := &ECDSASignature{}
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm))
h := f()
h.Write(data)
_, err := asn1.Unmarshal(sig, e)
if err != nil {
return false, ErrSigNotProvidedOrInvalid
}
return ecdsa.Verify(pubkey, h.Sum(nil), e.R, e.S), nil
}
// Verify RSA Public Key Signature
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
pubkey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(k.Modulus),
E: int(uint(k.Exponent[2]) | uint(k.Exponent[1])<<8 | uint(k.Exponent[0])<<16),
}
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm))
h := f()
h.Write(data)
var hash crypto.Hash
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) {
case AlgRS1:
hash = crypto.SHA1
case AlgPS256, AlgRS256:
hash = crypto.SHA256
case AlgPS384, AlgRS384:
hash = crypto.SHA384
case AlgPS512, AlgRS512:
hash = crypto.SHA512
default:
return false, ErrUnsupportedAlgorithm
}
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) {
case AlgPS256, AlgPS384, AlgPS512:
err := rsa.VerifyPSS(pubkey, hash, h.Sum(nil), sig, nil)
return err == nil, err
case AlgRS1, AlgRS256, AlgRS384, AlgRS512:
err := rsa.VerifyPKCS1v15(pubkey, hash, h.Sum(nil), sig)
return err == nil, err
default:
return false, ErrUnsupportedAlgorithm
}
}
// Return which signature algorithm is being used from the COSE Key
func SigAlgFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) SignatureAlgorithm {
for _, details := range SignatureAlgorithmDetails {
if details.coseAlg == coseAlg {
return details.algo
}
}
return UnknownSignatureAlgorithm
}
// Return the Hashing interface to be used for a given COSE Algorithm
func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) func() hash.Hash {
for _, details := range SignatureAlgorithmDetails {
if details.coseAlg == coseAlg {
return details.hasher
}
}
// default to SHA256? Why not.
return crypto.SHA256.New
}
// Figure out what kind of COSE material was provided and create the data for the new key
func ParsePublicKey(keyBytes []byte) (interface{}, error) {
pk := PublicKeyData{}
cbor.Unmarshal(keyBytes, &pk)
switch COSEKeyType(pk.KeyType) {
case OctetKey:
var o OKPPublicKeyData
cbor.Unmarshal(keyBytes, &o)
o.PublicKeyData = pk
return o, nil
case EllipticKey:
var e EC2PublicKeyData
cbor.Unmarshal(keyBytes, &e)
e.PublicKeyData = pk
return e, nil
case RSAKey:
var r RSAPublicKeyData
cbor.Unmarshal(keyBytes, &r)
r.PublicKeyData = pk
return r, nil
default:
return nil, ErrUnsupportedKey
}
}
// ParseFIDOPublicKey is only used when the appID extension is configured by the assertion response.
func ParseFIDOPublicKey(keyBytes []byte) (EC2PublicKeyData, error) {
x, y := elliptic.Unmarshal(elliptic.P256(), keyBytes)
return EC2PublicKeyData{
PublicKeyData: PublicKeyData{
Algorithm: int64(AlgES256),
},
XCoord: x.Bytes(),
YCoord: y.Bytes(),
}, nil
}
// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm
// identifiers SHOULD be values registered in the IANA COSE Algorithms registry
// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256"
// and -257 for "RS256".
type COSEAlgorithmIdentifier int
const (
// AlgES256 ECDSA with SHA-256
AlgES256 COSEAlgorithmIdentifier = -7
// AlgES384 ECDSA with SHA-384
AlgES384 COSEAlgorithmIdentifier = -35
// AlgES512 ECDSA with SHA-512
AlgES512 COSEAlgorithmIdentifier = -36
// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1
AlgRS1 COSEAlgorithmIdentifier = -65535
// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256
AlgRS256 COSEAlgorithmIdentifier = -257
// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384
AlgRS384 COSEAlgorithmIdentifier = -258
// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512
AlgRS512 COSEAlgorithmIdentifier = -259
// AlgPS256 RSASSA-PSS with SHA-256
AlgPS256 COSEAlgorithmIdentifier = -37
// AlgPS384 RSASSA-PSS with SHA-384
AlgPS384 COSEAlgorithmIdentifier = -38
// AlgPS512 RSASSA-PSS with SHA-512
AlgPS512 COSEAlgorithmIdentifier = -39
// AlgEdDSA EdDSA
AlgEdDSA COSEAlgorithmIdentifier = -8
)
// The Key Type derived from the IANA COSE AuthData
type COSEKeyType int
const (
// OctetKey is an Octet Key
OctetKey COSEKeyType = 1
// EllipticKey is an Elliptic Curve Public Key
EllipticKey COSEKeyType = 2
// RSAKey is an RSA Public Key
RSAKey COSEKeyType = 3
)
func VerifySignature(key interface{}, data []byte, sig []byte) (bool, error) {
switch key.(type) {
case OKPPublicKeyData:
o := key.(OKPPublicKeyData)
return o.Verify(data, sig)
case EC2PublicKeyData:
e := key.(EC2PublicKeyData)
return e.Verify(data, sig)
case RSAPublicKeyData:
r := key.(RSAPublicKeyData)
return r.Verify(data, sig)
default:
return false, ErrUnsupportedKey
}
}
func DisplayPublicKey(cpk []byte) string {
parsedKey, err := ParsePublicKey(cpk)
if err != nil {
return "Cannot display key"
}
switch parsedKey.(type) {
case RSAPublicKeyData:
pKey := parsedKey.(RSAPublicKeyData)
rKey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(pKey.Modulus),
E: int(uint(pKey.Exponent[2]) | uint(pKey.Exponent[1])<<8 | uint(pKey.Exponent[0])<<16),
}
data, err := x509.MarshalPKIXPublicKey(rKey)
if err != nil {
return "Cannot display key"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: data,
})
return fmt.Sprintf("%s", pemBytes)
case EC2PublicKeyData:
pKey := parsedKey.(EC2PublicKeyData)
var curve elliptic.Curve
switch COSEAlgorithmIdentifier(pKey.Algorithm) {
case AlgES256:
curve = elliptic.P256()
case AlgES384:
curve = elliptic.P384()
case AlgES512:
curve = elliptic.P521()
default:
return "Cannot display key"
}
eKey := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(pKey.XCoord),
Y: big.NewInt(0).SetBytes(pKey.YCoord),
}
data, err := x509.MarshalPKIXPublicKey(eKey)
if err != nil {
return "Cannot display key"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
})
return fmt.Sprintf("%s", pemBytes)
case OKPPublicKeyData:
pKey := parsedKey.(OKPPublicKeyData)
if len(pKey.XCoord) != ed25519.PublicKeySize {
return "Cannot display key"
}
var oKey ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(oKey, pKey.XCoord)
data, err := marshalEd25519PublicKey(oKey)
if err != nil {
return "Cannot display key"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
})
return fmt.Sprintf("%s", pemBytes)
default:
return "Cannot display key of this type"
}
}
// Algorithm enumerations used for
type SignatureAlgorithm int
const (
UnknownSignatureAlgorithm SignatureAlgorithm = iota
MD2WithRSA
MD5WithRSA
SHA1WithRSA
SHA256WithRSA
SHA384WithRSA
SHA512WithRSA
DSAWithSHA1
DSAWithSHA256
ECDSAWithSHA1
ECDSAWithSHA256
ECDSAWithSHA384
ECDSAWithSHA512
SHA256WithRSAPSS
SHA384WithRSAPSS
SHA512WithRSAPSS
)
var SignatureAlgorithmDetails = []struct {
algo SignatureAlgorithm
coseAlg COSEAlgorithmIdentifier
name string
hasher func() hash.Hash
}{
{SHA1WithRSA, AlgRS1, "SHA1-RSA", crypto.SHA1.New},
{SHA256WithRSA, AlgRS256, "SHA256-RSA", crypto.SHA256.New},
{SHA384WithRSA, AlgRS384, "SHA384-RSA", crypto.SHA384.New},
{SHA512WithRSA, AlgRS512, "SHA512-RSA", crypto.SHA512.New},
{SHA256WithRSAPSS, AlgPS256, "SHA256-RSAPSS", crypto.SHA256.New},
{SHA384WithRSAPSS, AlgPS384, "SHA384-RSAPSS", crypto.SHA384.New},
{SHA512WithRSAPSS, AlgPS512, "SHA512-RSAPSS", crypto.SHA512.New},
{ECDSAWithSHA256, AlgES256, "ECDSA-SHA256", crypto.SHA256.New},
{ECDSAWithSHA384, AlgES384, "ECDSA-SHA384", crypto.SHA384.New},
{ECDSAWithSHA512, AlgES512, "ECDSA-SHA512", crypto.SHA512.New},
{UnknownSignatureAlgorithm, AlgEdDSA, "EdDSA", crypto.SHA512.New},
}
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"`
}
var (
ErrUnsupportedKey = &Error{
Type: "invalid_key_type",
Details: "Unsupported Public Key Type",
}
ErrUnsupportedAlgorithm = &Error{
Type: "unsupported_key_algorithm",
Details: "Unsupported public key algorithm",
}
ErrSigNotProvidedOrInvalid = &Error{
Type: "signature_not_provided_or_invalid",
Details: "Signature invalid or not provided",
}
)
func (err *Error) Error() string {
return err.Details
}
func (passedError *Error) WithDetails(details string) *Error {
err := *passedError
err.Details = details
return &err
}

@ -0,0 +1,51 @@
package webauthn
import (
p "github.com/duo-labs/webauthn/protocol"
)
type Authenticator struct {
// The AAGUID of the authenticator. An AAGUID is defined as an array containing the globally unique
// identifier of the authenticator model being sought.
AAGUID []byte
// SignCount -Upon a new login operation, the Relying Party compares the stored signature counter value
// with the new signCount value returned in the assertion’s authenticator data. If this new
// signCount value is less than or equal to the stored value, a cloned authenticator may
// exist, or the authenticator may be malfunctioning.
SignCount uint32
// CloneWarning - This is a signal that the authenticator may be cloned, i.e. at least two copies of the
// credential private key may exist and are being used in parallel. Relying Parties should incorporate
// this information into their risk scoring. Whether the Relying Party updates the stored signature
// counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.
CloneWarning bool
}
// Allow for easy marhsalling of authenticator options that are provided to the user
func SelectAuthenticator(att string, rrk *bool, uv string) p.AuthenticatorSelection {
return p.AuthenticatorSelection{
AuthenticatorAttachment: p.AuthenticatorAttachment(att),
RequireResidentKey: rrk,
UserVerification: p.UserVerificationRequirement(uv),
}
}
// VerifyCounter
// Step 17 of §7.2. about verifying attestation. If the signature counter value authData.signCount
// is nonzero or the value stored in conjunction with credential’s id attribute is nonzero, then
// run the following sub-step:
//
// If the signature counter value authData.signCount is
//
// → Greater than the signature counter value stored in conjunction with credential’s id attribute.
// Update the stored signature counter value, associated with credential’s id attribute, to be the value of
// authData.signCount.
//
// → Less than or equal to the signature counter value stored in conjunction with credential’s id attribute.
// This is a signal that the authenticator may be cloned, see CloneWarning above for more information.
func (a *Authenticator) UpdateCounter(authDataCount uint32) {
if authDataCount <= a.SignCount && (authDataCount != 0 || a.SignCount != 0) {
a.CloneWarning = true
return
}
a.SignCount = authDataCount
}

@ -0,0 +1,35 @@
package webauthn
import (
"github.com/duo-labs/webauthn/protocol"
)
// Credential contains all needed information about a WebAuthn credential for storage
type Credential struct {
// A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions.
ID []byte
// The public key portion of a Relying Party-specific credential key pair, generated by an authenticator and returned to
// a Relying Party at registration time (see also public key credential). The private key portion of the credential key
// pair is known as the credential private key. Note that in the case of self attestation, the credential key pair is also
// used as the attestation key pair, see self attestation for details.
PublicKey []byte
// The attestation format used (if any) by the authenticator when creating the credential.
AttestationType string
// The Authenticator information for a given certificate
Authenticator Authenticator
}
// MakeNewCredential will return a credential pointer on successful validation of a registration response
func MakeNewCredential(c *protocol.ParsedCredentialCreationData) (*Credential, error) {
newCredential := &Credential{
ID: c.Response.AttestationObject.AuthData.AttData.CredentialID,
PublicKey: c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey,
AttestationType: c.Response.AttestationObject.Format,
Authenticator: Authenticator{
AAGUID: c.Response.AttestationObject.AuthData.AttData.AAGUID,
SignCount: c.Response.AttestationObject.AuthData.Counter,
},
}
return newCredential, nil
}

@ -0,0 +1,3 @@
// Contains the API functionality of the library. After creating and configuring a webauthn object, users can
// call the object to create and validate web authentication credentials.
package webauthn

@ -0,0 +1,188 @@
package webauthn
import (
"bytes"
"encoding/base64"
"net/http"
"github.com/duo-labs/webauthn/protocol"
)
// BEGIN REGISTRATION
// These objects help us creat the CredentialCreationOptions
// that will be passed to the authenticator via the user client
// LoginOption is used to provide parameters that modify the default Credential Assertion Payload that is sent to the user.
type LoginOption func(*protocol.PublicKeyCredentialRequestOptions)
// Creates the CredentialAssertion data payload that should be sent to the user agent for beginning the
// login/assertion process. The format of this data can be seen in §5.5 of the WebAuthn specification
// (https://www.w3.org/TR/webauthn/#assertion-options). These default values can be amended by providing
// additional LoginOption parameters. This function also returns sessionData, that must be stored by the
// RP in a secure manner and then provided to the FinishLogin function. This data helps us verify the
// ownership of the credential being retreived.
func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
challenge, err := protocol.CreateChallenge()
if err != nil {
return nil, nil, err
}
credentials := user.WebAuthnCredentials()
if len(credentials) == 0 { // If the user does not have any credentials, we cannot do login
return nil, nil, protocol.ErrBadRequest.WithDetails("Found no credentials for user")
}
var allowedCredentials = make([]protocol.CredentialDescriptor, len(credentials))
for i, credential := range credentials {
var credentialDescriptor protocol.CredentialDescriptor
credentialDescriptor.CredentialID = credential.ID
credentialDescriptor.Type = protocol.PublicKeyCredentialType
allowedCredentials[i] = credentialDescriptor
}
requestOptions := protocol.PublicKeyCredentialRequestOptions{
Challenge: challenge,
Timeout: webauthn.Config.Timeout,
RelyingPartyID: webauthn.Config.RPID,
UserVerification: webauthn.Config.AuthenticatorSelection.UserVerification,
AllowedCredentials: allowedCredentials,
}
for _, setter := range opts {
setter(&requestOptions)
}
newSessionData := SessionData{
Challenge: base64.RawURLEncoding.EncodeToString(challenge),
UserID: user.WebAuthnID(),
AllowedCredentialIDs: requestOptions.GetAllowedCredentialIDs(),
UserVerification: requestOptions.UserVerification,
Extensions: requestOptions.Extensions,
}
response := protocol.CredentialAssertion{requestOptions}
return &response, &newSessionData, nil
}
// Updates the allowed credential list with Credential Descripiptors, discussed in §5.10.3
// (https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor) with user-supplied values
func WithAllowedCredentials(allowList []protocol.CredentialDescriptor) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.AllowedCredentials = allowList
}
}
// Request a user verification preference
func WithUserVerification(userVerification protocol.UserVerificationRequirement) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.UserVerification = userVerification
}
}
// Request additional extensions for assertion
func WithAssertionExtensions(extensions protocol.AuthenticationExtensions) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.Extensions = extensions
}
}
// Take the response from the client and validate it against the user credentials and stored session data
func (webauthn *WebAuthn) FinishLogin(user User, session SessionData, response *http.Request) (*Credential, error) {
parsedResponse, err := protocol.ParseCredentialRequestResponse(response)
if err != nil {
return nil, err
}
return webauthn.ValidateLogin(user, session, parsedResponse)
}
// ValidateLogin takes a parsed response and validates it against the user credentials and session data
func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) {
if !bytes.Equal(user.WebAuthnID(), session.UserID) {
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session")
}
// Step 1. If the allowCredentials option was given when this authentication ceremony was initiated,
// verify that credential.id identifies one of the public key credentials that were listed in
// allowCredentials.
// NON-NORMATIVE Prior Step: Verify that the allowCredentials for the session are owned by the user provided
userCredentials := user.WebAuthnCredentials()
var credentialFound bool
if len(session.AllowedCredentialIDs) > 0 {
var credentialsOwned bool
for _, allowedCredentialID := range session.AllowedCredentialIDs {
for _, userCredential := range userCredentials {
if bytes.Equal(userCredential.ID, allowedCredentialID) {
credentialsOwned = true
break
}
credentialsOwned = false
}
}
if !credentialsOwned {
return nil, protocol.ErrBadRequest.WithDetails("User does not own all credentials from the allowedCredentialList")
}
for _, allowedCredentialID := range session.AllowedCredentialIDs {
if bytes.Equal(parsedResponse.RawID, allowedCredentialID) {
credentialFound = true
break
}
}
if !credentialFound {
return nil, protocol.ErrBadRequest.WithDetails("User does not own the credential returned")
}
}
// Step 2. If credential.response.userHandle is present, verify that the user identified by this value is
// the owner of the public key credential identified by credential.id.
// This is in part handled by our Step 1
userHandle := parsedResponse.Response.UserHandle
if userHandle != nil && len(userHandle) > 0 {
if !bytes.Equal(userHandle, user.WebAuthnID()) {
return nil, protocol.ErrBadRequest.WithDetails("userHandle and User ID do not match")
}
}
// Step 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is inappropriate
// for your use case), look up the corresponding credential public key.
var loginCredential Credential
for _, cred := range userCredentials {
if bytes.Equal(cred.ID, parsedResponse.RawID) {
loginCredential = cred
credentialFound = true
break
}
credentialFound = false
}
if !credentialFound {
return nil, protocol.ErrBadRequest.WithDetails("Unable to find the credential for the returned credential ID")
}
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired
rpID := webauthn.Config.RPID
rpOrigin := webauthn.Config.RPOrigin
appID, err := parsedResponse.GetAppID(session.Extensions, loginCredential.AttestationType)
if err != nil {
return nil, err
}
// Handle steps 4 through 16
validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigin, appID, shouldVerifyUser, loginCredential.PublicKey)
if validError != nil {
return nil, validError
}
// Handle step 17
loginCredential.Authenticator.UpdateCounter(parsedResponse.Response.AuthenticatorData.Counter)
return &loginCredential, nil
}

@ -0,0 +1,71 @@
package webauthn
import (
"fmt"
"net/url"
"github.com/duo-labs/webauthn/protocol"
)
var defaultTimeout = 60000
// WebAuthn is the primary interface of this package and contains the request handlers that should be called.
type WebAuthn struct {
Config *Config
}
// The config values required for proper
type Config struct {
RPDisplayName string
RPID string
RPOrigin string
RPIcon string
// Defaults for generating options
AttestationPreference protocol.ConveyancePreference
AuthenticatorSelection protocol.AuthenticatorSelection
Timeout int
Debug bool
}
// Validate that the config flags in Config are properly set
func (config *Config) validate() error {
if len(config.RPDisplayName) == 0 {
return fmt.Errorf("Missing RPDisplayName")
}
if len(config.RPID) == 0 {
return fmt.Errorf("Missing RPID")
}
_, err := url.Parse(config.RPID)
if err != nil {
return fmt.Errorf("RPID not valid URI: %+v", err)
}
if config.Timeout == 0 {
config.Timeout = defaultTimeout
}
if config.RPOrigin == "" {
config.RPOrigin = config.RPID
} else {
u, err := url.Parse(config.RPOrigin)
if err != nil {
return fmt.Errorf("RPOrigin not valid URL: %+v", err)
}
config.RPOrigin = protocol.FullyQualifiedOrigin(u)
}
return nil
}
// Create a new WebAuthn object given the proper config flags
func New(config *Config) (*WebAuthn, error) {
if err := config.validate(); err != nil {
return nil, fmt.Errorf("Configuration error: %+v", err)
}
return &WebAuthn{
config,
}, nil
}

@ -0,0 +1,170 @@
package webauthn
import (
"bytes"
"encoding/base64"
"net/http"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
// BEGIN REGISTRATION
// These objects help us creat the CredentialCreationOptions
// that will be passed to the authenticator via the user client
type RegistrationOption func(*protocol.PublicKeyCredentialCreationOptions)
// Generate a new set of registration data to be sent to the client and authenticator.
func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOption) (*protocol.CredentialCreation, *SessionData, error) {
challenge, err := protocol.CreateChallenge()
if err != nil {
return nil, nil, err
}
webAuthnUser := protocol.UserEntity{
ID: user.WebAuthnID(),
DisplayName: user.WebAuthnDisplayName(),
CredentialEntity: protocol.CredentialEntity{
Name: user.WebAuthnName(),
Icon: user.WebAuthnIcon(),
},
}
relyingParty := protocol.RelyingPartyEntity{
ID: webauthn.Config.RPID,
CredentialEntity: protocol.CredentialEntity{
Name: webauthn.Config.RPDisplayName,
Icon: webauthn.Config.RPIcon,
},
}
credentialParams := defaultRegistrationCredentialParameters()
creationOptions := protocol.PublicKeyCredentialCreationOptions{
Challenge: challenge,
RelyingParty: relyingParty,
User: webAuthnUser,
Parameters: credentialParams,
AuthenticatorSelection: webauthn.Config.AuthenticatorSelection,
Timeout: webauthn.Config.Timeout,
Attestation: webauthn.Config.AttestationPreference,
}
for _, setter := range opts {
setter(&creationOptions)
}
response := protocol.CredentialCreation{Response: creationOptions}
newSessionData := SessionData{
Challenge: base64.RawURLEncoding.EncodeToString(challenge),
UserID: user.WebAuthnID(),
UserVerification: creationOptions.AuthenticatorSelection.UserVerification,
}
if err != nil {
return nil, nil, protocol.ErrParsingData.WithDetails("Error packing session data")
}
return &response, &newSessionData, nil
}
// Provide non-default parameters regarding the authenticator to select.
func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSelection) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.AuthenticatorSelection = authenticatorSelection
}
}
// Provide non-default parameters regarding credentials to exclude from retrieval.
func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.CredentialExcludeList = excludeList
}
}
// Provide non-default parameters regarding whether the authenticator should attest to the credential.
func WithConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Attestation = preference
}
}
// Provide extension parameter to registration options
func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Extensions = extension
}
}
// Take the response from the authenticator and client and verify the credential against the user's credentials and
// session data.
func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, response *http.Request) (*Credential, error) {
parsedResponse, err := protocol.ParseCredentialCreationResponse(response)
if err != nil {
return nil, err
}
return webauthn.CreateCredential(user, session, parsedResponse)
}
// CreateCredential verifies a parsed response against the user's credentials and session data.
func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parsedResponse *protocol.ParsedCredentialCreationData) (*Credential, error) {
if !bytes.Equal(user.WebAuthnID(), session.UserID) {
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session")
}
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired
invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigin)
if invalidErr != nil {
return nil, invalidErr
}
return MakeNewCredential(parsedResponse)
}
func defaultRegistrationCredentialParameters() []protocol.CredentialParameter {
return []protocol.CredentialParameter{
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES256,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES384,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES512,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS256,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS384,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS512,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS256,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS384,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS512,
},
protocol.CredentialParameter{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgEdDSA,
},
}
}

@ -0,0 +1,13 @@
package webauthn
import "github.com/duo-labs/webauthn/protocol"
// SessionData is the data that should be stored by the Relying Party for
// the duration of the web authentication ceremony
type SessionData struct {
Challenge string `json:"challenge"`
UserID []byte `json:"user_id"`
AllowedCredentialIDs [][]byte `json:"allowed_credentials,omitempty"`
UserVerification protocol.UserVerificationRequirement `json:"userVerification"`
Extensions protocol.AuthenticationExtensions `json:"extensions,omitempty"`
}

@ -0,0 +1,42 @@
package webauthn
// User is built to interface with the Relying Party's User entry and
// elaborate the fields and methods needed for WebAuthn
type User interface {
// User ID according to the Relying Party
WebAuthnID() []byte
// User Name according to the Relying Party
WebAuthnName() string
// Display Name of the user
WebAuthnDisplayName() string
// User's icon url
WebAuthnIcon() string
// Credentials owned by the user
WebAuthnCredentials() []Credential
}
type defaultUser struct {
id []byte
}
var _ User = (*defaultUser)(nil)
func (user *defaultUser) WebAuthnID() []byte {
return user.id
}
func (user *defaultUser) WebAuthnName() string {
return "newUser"
}
func (user *defaultUser) WebAuthnDisplayName() string {
return "New User"
}
func (user *defaultUser) WebAuthnIcon() string {
return "https://pics.com/avatar.png"
}
func (user *defaultUser) WebAuthnCredentials() []Credential {
return []Credential{}
}

@ -0,0 +1,12 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

@ -0,0 +1,86 @@
# Do not delete linter settings. Linters like gocritic can be enabled on the command line.
linters-settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 3
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- paramTypeCombine
- whyNoLint
- wrapperFunc
gofmt:
simplify: false
goimports:
local-prefixes: github.com/fxamacker/cbor
golint:
min-confidence: 0
govet:
check-shadowing: true
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
linters:
disable-all: true
enable:
- deadcode
- errcheck
- goconst
- gocyclo
- gofmt
- goimports
- golint
- gosec
- govet
- ineffassign
- maligned
- misspell
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
issues:
# max-issues-per-linter default is 50. Set to 0 to disable limit.
max-issues-per-linter: 0
# max-same-issues default is 3. Set to 0 to disable limit.
max-same-issues: 0
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- goconst
- dupl
- gomnd
- lll
- path: doc\.go
linters:
- goimports
- gomnd
- lll
# golangci.com configuration
# https://github.com/golangci/golangci/wiki/Configuration
service:
golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly

@ -0,0 +1,264 @@
# CBOR Benchmarks for fxamacker/cbor
See [bench_test.go](bench_test.go).
Benchmarks on Feb. 22, 2020 with cbor v2.2.0:
* [Go builtin types](#go-builtin-types)
* [Go structs](#go-structs)
* [Go structs with "keyasint" struct tag](#go-structs-with-keyasint-struct-tag)
* [Go structs with "toarray" struct tag](#go-structs-with-toarray-struct-tag)
* [COSE data](#cose-data)
* [CWT claims data](#cwt-claims-data)
* [SenML data](#SenML-data)
## Go builtin types
Benchmarks use data representing the following values:
* Boolean: `true`
* Positive integer: `18446744073709551615`
* Negative integer: `-1000`
* Float: `-4.1`
* Byte string: `h'0102030405060708090a0b0c0d0e0f101112131415161718191a'`
* Text string: `"The quick brown fox jumps over the lazy dog"`
* Array: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]`
* Map: `{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}}`
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshal/CBOR_bool_to_Go_interface_{}-2 | 110 ns/op | 16 B/op | 1 allocs/op
BenchmarkUnmarshal/CBOR_bool_to_Go_bool-2 | 99.3 ns/op | 1 B/op | 1 allocs/op
BenchmarkUnmarshal/CBOR_positive_int_to_Go_interface_{}-2 | 135 ns/op | 24 B/op | 2 allocs/op
BenchmarkUnmarshal/CBOR_positive_int_to_Go_uint64-2 | 116 ns/op | 8 B/op | 1 allocs/op
BenchmarkUnmarshal/CBOR_negative_int_to_Go_interface_{}-2 | 133 ns/op | 24 B/op | 2 allocs/op
BenchmarkUnmarshal/CBOR_negative_int_to_Go_int64-2 | 113 ns/op | 8 B/op | 1 allocs/op
BenchmarkUnmarshal/CBOR_float_to_Go_interface_{}-2 | 137 ns/op | 24 B/op | 2 allocs/op
BenchmarkUnmarshal/CBOR_float_to_Go_float64-2 | 115 ns/op | 8 B/op | 1 allocs/op
BenchmarkUnmarshal/CBOR_bytes_to_Go_interface_{}-2 | 179 ns/op | 80 B/op | 3 allocs/op
BenchmarkUnmarshal/CBOR_bytes_to_Go_[]uint8-2 | 194 ns/op | 64 B/op | 2 allocs/op
BenchmarkUnmarshal/CBOR_text_to_Go_interface_{}-2 | 209 ns/op | 80 B/op | 3 allocs/op
BenchmarkUnmarshal/CBOR_text_to_Go_string-2 | 193 ns/op | 64 B/op | 2 allocs/op
BenchmarkUnmarshal/CBOR_array_to_Go_interface_{}-2 |1068 ns/op | 672 B/op | 29 allocs/op
BenchmarkUnmarshal/CBOR_array_to_Go_[]int-2 | 1073 ns/op | 272 B/op | 3 allocs/op
BenchmarkUnmarshal/CBOR_map_to_Go_interface_{}-2 | 2926 ns/op | 1420 B/op | 30 allocs/op
BenchmarkUnmarshal/CBOR_map_to_Go_map[string]interface_{}-2 | 3755 ns/op | 965 B/op | 19 allocs/op
BenchmarkUnmarshal/CBOR_map_to_Go_map[string]string-2 | 2586 ns/op | 740 B/op | 5 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshal/Go_bool_to_CBOR_bool-2 | 86.1 ns/op | 1 B/op | 1 allocs/op
BenchmarkMarshal/Go_uint64_to_CBOR_positive_int-2 | 97.0 ns/op | 16 B/op | 1 allocs/op
BenchmarkMarshal/Go_int64_to_CBOR_negative_int-2 | 90.3 ns/op | 3 B/op | 1 allocs/op
BenchmarkMarshal/Go_float64_to_CBOR_float-2 | 97.9 ns/op | 16 B/op | 1 allocs/op
BenchmarkMarshal/Go_[]uint8_to_CBOR_bytes-2 | 121 ns/op | 32 B/op | 1 allocs/op
BenchmarkMarshal/Go_string_to_CBOR_text-2 | 115 ns/op | 48 B/op | 1 allocs/op
BenchmarkMarshal/Go_[]int_to_CBOR_array-2 | 529 ns/op | 32 B/op | 1 allocs/op
BenchmarkMarshal/Go_map[string]string_to_CBOR_map-2 | 2115 ns/op | 576 B/op | 28 allocs/op
## Go structs
Benchmarks use struct and map[string]interface{} representing the following value:
```
{
"T": true,
"Ui": uint(18446744073709551615),
"I": -1000,
"F": -4.1,
"B": []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
"S": "The quick brown fox jumps over the lazy dog",
"Slci": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
"Mss": map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"},
}
```
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshal/CBOR_map_to_Go_map[string]interface{}-2 | 6221 ns/op | 2621 B/op | 73 allocs/op
BenchmarkUnmarshal/CBOR_map_to_Go_struct-2 | 4458 ns/op | 1172 B/op | 10 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshal/Go_map[string]interface{}_to_CBOR_map-2 | 4441 ns/op | 1072 B/op | 45 allocs/op
BenchmarkMarshal/Go_struct_to_CBOR_map-2 | 2866 ns/op | 720 B/op | 28 allocs/op
## Go structs with "keyasint" struct tag
Benchmarks use struct (with keyasint struct tag) and map[int]interface{} representing the following value:
```
{
1: true,
2: uint(18446744073709551615),
3: -1000,
4: -4.1,
5: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
6: "The quick brown fox jumps over the lazy dog",
7: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
8: map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"},
}
```
Struct type with keyasint struct tag is used to handle CBOR map with integer keys.
```
type T struct {
T bool `cbor:"1,keyasint"`
Ui uint `cbor:"2,keyasint"`
I int `cbor:"3,keyasint"`
F float64 `cbor:"4,keyasint"`
B []byte `cbor:"5,keyasint"`
S string `cbor:"6,keyasint"`
Slci []int `cbor:"7,keyasint"`
Mss map[string]string `cbor:"8,keyasint"`
}
```
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshal/CBOR_map_to_Go_map[int]interface{}-2| 6030 ns/op | 2517 B/op | 70 allocs/op
BenchmarkUnmarshal/CBOR_map_to_Go_struct_keyasint-2 | 4332 ns/op | 1173 B/op | 10 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshal/Go_map[int]interface{}_to_CBOR_map-2 | 4348 ns/op | 992 B/op | 45 allocs/op
BenchmarkMarshal/Go_struct_keyasint_to_CBOR_map-2 | 2847 ns/op | 704 B/op | 28 allocs/op
## Go structs with "toarray" struct tag
Benchmarks use struct (with toarray struct tag) and []interface{} representing the following value:
```
[
true,
uint(18446744073709551615),
-1000,
-4.1,
[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
"The quick brown fox jumps over the lazy dog",
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}
]
```
Struct type with toarray struct tag is used to handle CBOR array.
```
type T struct {
_ struct{} `cbor:",toarray"`
T bool
Ui uint
I int
F float64
B []byte
S string
Slci []int
Mss map[string]string
}
```
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshal/CBOR_array_to_Go_[]interface{}-2 | 4863 ns/op | 2404 B/op | 67 allocs/op
BenchmarkUnmarshal/CBOR_array_to_Go_struct_toarray-2 | 4173 ns/op | 1164 B/op | 9 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshal/Go_[]interface{}_to_CBOR_map-2 | 3240 ns/op | 704 B/op | 28 allocs/op
BenchmarkMarshal/Go_struct_toarray_to_CBOR_array-2 | 2823 ns/op | 704 B/op | 28 allocs/op
## COSE data
Benchmarks use COSE data from https://tools.ietf.org/html/rfc8392#appendix-A section A.2
```
// 128-Bit Symmetric COSE_Key
{
/ k / -1: h'231f4c4d4d3051fdc2ec0a3851d5b383'
/ kty / 1: 4 / Symmetric /,
/ kid / 2: h'53796d6d6574726963313238' / 'Symmetric128' /,
/ alg / 3: 10 / AES-CCM-16-64-128 /
}
// 256-Bit Symmetric COSE_Key
{
/ k / -1: h'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1
ec99192d79569388'
/ kty / 1: 4 / Symmetric /,
/ kid / 4: h'53796d6d6574726963323536' / 'Symmetric256' /,
/ alg / 3: 4 / HMAC 256/64 /
}
// ECDSA 256-Bit COSE Key
{
/ d / -4: h'6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e
6c67c858bc206c19',
/ y / -3: h'60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168
db9529971a36e7b9',
/ x / -2: h'143329cce7868e416927599cf65a34f3ce2ffda55a7eca69
ed8919a394d42f0f',
/ crv / -1: 1 / P-256 /,
/ kty / 1: 2 / EC2 /,
/ kid / 2: h'4173796d6d657472696345434453413
23536' / 'AsymmetricECDSA256' /,
/ alg / 3: -7 / ECDSA 256 /
}
```
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshalCOSE/128-Bit_Symmetric_Key-2 | 562 ns/op | 240 B/op | 4 allocs/op
BenchmarkUnmarshalCOSE/256-Bit_Symmetric_Key-2 | 568 ns/op | 256 B/op | 4 allocs/op
BenchmarkUnmarshalCOSE/ECDSA_P256_256-Bit_Key-2 | 968 ns/op | 360 B/op | 7 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshalCOSE/128-Bit_Symmetric_Key-2 | 523 ns/op | 224 B/op | 2 allocs/op
BenchmarkMarshalCOSE/256-Bit_Symmetric_Key-2 | 521 ns/op | 240 B/op | 2 allocs/op
BenchmarkMarshalCOSE/ECDSA_P256_256-Bit_Key-2 | 668 ns/op | 320 B/op | 2 allocs/op
## CWT claims data
Benchmarks use CTW claims data from https://tools.ietf.org/html/rfc8392#appendix-A section A.1
```
{
/ iss / 1: "coap://as.example.com",
/ sub / 2: "erikw",
/ aud / 3: "coap://light.example.com",
/ exp / 4: 1444064944,
/ nbf / 5: 1443944944,
/ iat / 6: 1443944944,
/ cti / 7: h'0b71'
}
```
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshalCWTClaims-2 | 765 ns/op | 176 B/op | 6 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshalCWTClaims-2 | 451 ns/op | 176 B/op | 2 allocs/op
## SenML data
Benchmarks use SenML data from https://tools.ietf.org/html/rfc8428#section-6
```
[
{-2: "urn:dev:ow:10e2073a0108006:", -3: 1276020076.001, -4: "A", -1: 5, 0: "voltage", 1: "V", 2: 120.1},
{0: "current", 6: -5, 2: 1.2},
{0: "current", 6: -4, 2: 1.3},
{0: "current", 6: -3, 2: 1.4},
{0: "current", 6: -2, 2: 1.5},
{0: "current", 6: -1, 2: 1.6},
{0: "current", 6: 0, 2: 1.7}
]
```
Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkUnmarshalSenML-2 | 3106 ns/op | 1544 B/op | 18 allocs/op
Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshalSenML-2 | 2976 ns/op | 272 B/op | 2 allocs/op

@ -0,0 +1,32 @@
👉 [Comparisons](https://github.com/fxamacker/cbor#comparisons) • [Status](https://github.com/fxamacker/cbor#current-status) • [Design Goals](https://github.com/fxamacker/cbor#design-goals) • [Features](https://github.com/fxamacker/cbor#features) • [Standards](https://github.com/fxamacker/cbor#standards) • [Fuzzing](https://github.com/fxamacker/cbor#fuzzing-and-code-coverage) • [Usage](https://github.com/fxamacker/cbor#usage) • [Security Policy](https://github.com/fxamacker/cbor#security-policy) • [License](https://github.com/fxamacker/cbor#license)
# CBOR
[CBOR](https://en.wikipedia.org/wiki/CBOR) is a data format designed to allow small code size and small message size. CBOR is defined in [RFC 7049 Concise Binary Object Representation](https://tools.ietf.org/html/rfc7049), an [IETF](http://ietf.org/) Internet Standards Document.
CBOR is also designed to be stable for decades, be extensible without need for version negotiation, and not require a schema.
While JSON uses text, CBOR uses binary. CDDL can be used to express CBOR (and JSON) in an easy and unambiguous way. CDDL is defined in (RFC 8610 Concise Data Definition Language).
## CBOR in Golang (Go)
[Golang](https://golang.org/) is a nickname for the Go programming language. Go is specified in [The Go Programming Language Specification](https://golang.org/ref/spec).
__[fxamacker/cbor](https://github.com/fxamacker/cbor)__ is a library (written in Go) that encodes and decodes CBOR. The API design of fxamacker/cbor is based on Go's [`encoding/json`](https://golang.org/pkg/encoding/json/). The design and reliability of fxamacker/cbor makes it ideal for encoding and decoding COSE.
## COSE
COSE is a protocol using CBOR for basic security services. COSE is defined in ([RFC 8152 CBOR Object Signing and Encryption](https://tools.ietf.org/html/rfc8152)).
COSE describes how to create and process signatures, message authentication codes, and encryption using CBOR for serialization. COSE specification also describes how to represent cryptographic keys using CBOR. COSE is used by WebAuthn.
## CWT
CBOR Web Token (CWT) is defined in [RFC 8392](http://tools.ietf.org/html/rfc8392). CWT is based on COSE and was derived in part from JSON Web Token (JWT). CWT is a compact way to securely represent claims to be transferred between two parties.
## WebAuthn
[WebAuthn](https://en.wikipedia.org/wiki/WebAuthn) (Web Authentication) is a web standard for authenticating users to web-based apps and services. It's a core component of FIDO2, the successor of FIDO U2F legacy protocol.
__[fxamacker/webauthn](https://github.com/fxamacker/webauthn)__ is a library (written in Go) that performs server-side authentication for clients using FIDO2 keys, legacy FIDO U2F keys, tpm, and etc.
Copyright (c) Faye Amacker and contributors.
<hr>
👉 [Comparisons](https://github.com/fxamacker/cbor#comparisons) • [Status](https://github.com/fxamacker/cbor#current-status) • [Design Goals](https://github.com/fxamacker/cbor#design-goals) • [Features](https://github.com/fxamacker/cbor#features) • [Standards](https://github.com/fxamacker/cbor#standards) • [Fuzzing](https://github.com/fxamacker/cbor#fuzzing-and-code-coverage) • [Usage](https://github.com/fxamacker/cbor#usage) • [Security Policy](https://github.com/fxamacker/cbor#security-policy) • [License](https://github.com/fxamacker/cbor#license)

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at faye.github@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

@ -0,0 +1,47 @@
# How to contribute
This project started because I needed an easy, small, and crash-proof CBOR library for my [WebAuthn (FIDO2) server library](https://github.com/fxamacker/webauthn). I believe this was the first and still only standalone CBOR library (in Go) that is fuzz tested as of November 10, 2019.
To my surprise, Stefan Tatschner (rumpelsepp) submitted the first 2 issues when I didn't expect this project to be noticed. So I decided to make it more full-featured for others by announcing releases and asking for feedback. Even this document exists because Montgomery Edwards⁴⁴⁸ (x448) opened [issue #22](https://github.com/fxamacker/cbor/issues/22). In other words, you can contribute by opening an issue that helps the project improve. Especially in the early stages.
When I announced v1.2 on Go Forum, Jakob Borg (calmh) responded with a thumbs up and encouragement. Another project of equal priority needed my time and Jakob's kind words tipped the scale for me to work on this one (speedups for [milestone v1.3](https://github.com/fxamacker/cbor/issues?q=is%3Aopen+is%3Aissue+milestone%3Av1.3.0).) So words of appreciation or encouragement is nice way to contribute to open source projects.
Another way is by using this library in your project. It can lead to features that benefit both projects, which is what happened when oasislabs/oasis-core switched to this CBOR libary -- thanks Yawning Angel (yawning) for requesting BinaryMarshaler/BinaryUnmarshaler and Jernej Kos (kostco) for requesting RawMessage!
If you'd like to contribute code or send CBOR data, please read on (it can save you time!)
## Private reports
Usually, all issues are tracked publicly on [GitHub](https://github.com/fxamacker/cbor/issues).
To report security vulnerabilities, please email faye.github@gmail.com and allow time for the problem to be resolved before disclosing it to the public. For more info, see [Security Policy](https://github.com/fxamacker/cbor#security-policy).
Please do not send data that might contain personally identifiable information, even if you think you have permission. That type of support requires payment and a contract where I'm indemnified, held harmless, and defended for any data you send to me.
## Prerequisites to pull requests
Please [create an issue](https://github.com/fxamacker/cbor/issues/new/choose), if one doesn't already exist, and describe your concern. You'll need a [GitHub account](https://github.com/signup/free) to do this.
If you submit a pull request without creating an issue and getting a response, you risk having your work unused because the bugfix or feature was already done by others and being reviewed before reaching Github.
## Describe your issue
Clearly describe the issue:
* If it's a bug, please provide: **version of this library** and **Go** (`go version`), **unmodified error message**, and describe **how to reproduce it**. Also state **what you expected to happen** instead of the error.
* If you propose a change or addition, try to give an example how the improved code could look like or how to use it.
* If you found a compilation error, please confirm you're using a supported version of Go. If you are, then provide the output of `go version` first, followed by the complete error message.
## Please don't
Please don't send data containing personally identifiable information, even if you think you have permission. That type of support requires payment and a contract where I'm indemnified, held harmless, and defended for any data you send to me.
Please don't send CBOR data larger than 512 bytes. If you want to send crash-producing CBOR data > 512 bytes, please get my permission before sending it to me.
## Wanted
* Opening issues that are helpful to the project
* Using this library in your project and letting me know
* Sending well-formed CBOR data (<= 512 bytes) that causes crashes (none found yet).
* Sending malformed CBOR data (<= 512 bytes) that causes crashes (none found yet, but bad actors are better than me at breaking things).
* Sending tests or data for unit tests that increase code coverage (currently at 97.8% for v1.2.)
* Pull requests with small changes that are well-documented and easily understandable.
* Sponsors, donations, bounties, subscriptions: I'd like to run uninterrupted fuzzing between releases on a server with dedicated CPUs (after v1.3 or v1.4.)
## Credits
This guide used nlohmann/json contribution guidelines for inspiration as suggested in issue #22.

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - present Faye Amacker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,938 @@
[![CBOR Library - Slideshow and Latest Docs.](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_slides.gif)](https://github.com/fxamacker/cbor/blob/master/README.md)
# CBOR library in Go
[__`fxamacker/cbor`__](https://github.com/fxamacker/cbor) is a CBOR encoder & decoder in [Go](https://golang.org). It has a standard API, CBOR tags, options for duplicate map keys, float64→32→16, `toarray`, `keyasint`, etc. Each release passes 375+ tests and 250+ million execs fuzzing.
[![](https://github.com/fxamacker/cbor/workflows/ci/badge.svg)](https://github.com/fxamacker/cbor/actions?query=workflow%3Aci)
[![](https://github.com/fxamacker/cbor/workflows/cover%20%E2%89%A598%25/badge.svg)](https://github.com/fxamacker/cbor/actions?query=workflow%3A%22cover+%E2%89%A598%25%22)
[![](https://github.com/fxamacker/cbor/workflows/linters/badge.svg)](https://github.com/fxamacker/cbor/actions?query=workflow%3Alinters)
[![Go Report Card](https://goreportcard.com/badge/github.com/fxamacker/cbor)](https://goreportcard.com/report/github.com/fxamacker/cbor)
[![Release](https://img.shields.io/github/release/fxamacker/cbor.svg?style=flat-square)](https://github.com/fxamacker/cbor/releases)
[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/fxamacker/cbor/master/LICENSE)
__What is CBOR__? [CBOR](CBOR_GOLANG.md) ([RFC 7049](https://tools.ietf.org/html/rfc7049)) is a binary data format inspired by JSON and MessagePack. CBOR is used in [IETF](https://www.ietf.org) Internet Standards such as COSE ([RFC 8152](https://tools.ietf.org/html/rfc8152)) and CWT ([RFC 8392 CBOR Web Token](https://tools.ietf.org/html/rfc8392)). WebAuthn also uses CBOR.
__`fxamacker/cbor`__ is safe and fast. It safely handles malformed CBOR data:
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_security_table.svg?sanitize=1 "CBOR Security Comparison")
__`fxamacker/cbor`__ is fast when using CBOR data with Go structs:
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_speed_table.svg?sanitize=1 "CBOR Speed Comparison")
Benchmarks used data from [RFC 8392 Appendix A.1](https://tools.ietf.org/html/rfc8392#appendix-A.1) and default options for each CBOR library.
__`fxamacker/cbor`__ produces smaller binaries. All builds of cisco/senml had MessagePack feature removed:
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_size_comparison.png "CBOR library and program size comparison chart")
<hr>
__Standard API__: functions with signatures identical to [`encoding/json`](https://golang.org/pkg/encoding/json/) include:
`Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, and `decoder.Decode`.
__Standard interfaces__ allow custom encoding or decoding:
`BinaryMarshaler`, `BinaryUnmarshaler`, `Marshaler`, and `Unmarshaler`.
__Struct tags__ like __`toarray`__ & __`keyasint`__ translate Go struct fields to CBOR array elements, etc.
<br>
[![CBOR API](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_api_struct_tags.png)](#usage)
<hr>
__`fxamacker/cbor`__ is a full-featured CBOR encoder and decoder. Support for CBOR includes:
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_features.svg?sanitize=1 "CBOR Features")
<hr>
⚓ [__Installation__](#installation) • [__System Requirements__](#system-requirements) • [__Quick Start Guide__](#quick-start)
<hr>
__Why this CBOR library?__ It doesn't crash and it has well-balanced qualities: small, fast, safe and easy. It also has a standard API, CBOR tags (built-in and user-defined), float64→32→16, and duplicate map key options.
* __Standard API__. Codec functions with signatures identical to [`encoding/json`](https://golang.org/pkg/encoding/json/) include:
`Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, and `decoder.Decode`.
* __Customizable__. Standard interfaces are provided to allow user-implemented encoding or decoding:
`BinaryMarshaler`, `BinaryUnmarshaler`, `Marshaler`, and `Unmarshaler`.
* __Small apps__. Same programs are 4-9 MB smaller by switching to this library. No code gen and the only imported pkg is [x448/float16](https://github.com/x448/float16) which is maintained by the same team as this library.
* __Small data__. The `toarray`, `keyasint`, and `omitempty` struct tags shrink size of Go structs encoded to CBOR. Integers encode to smallest form that fits. Floats can shrink from float64 -> float32 -> float16 if values fit.
* __Fast__. v1.3 became faster than a well-known library that uses `unsafe` optimizations and code gen. Faster libraries will always exist, but speed is only one factor. This library doesn't use `unsafe` optimizations or code gen.
* __Safe__ and reliable. It prevents crashes on malicious CBOR data by using extensive tests, coverage-guided fuzzing, data validation, and avoiding Go's [`unsafe`](https://golang.org/pkg/unsafe/) pkg. Decoder settings include: `MaxNestedLevels`, `MaxArrayElements`, `MaxMapPairs`, and `IndefLength`.
* __Easy__ and saves time. Simple (no param) functions return preset `EncOptions` so you don't have to know the differences between Canonical CBOR and CTAP2 Canonical CBOR to use those standards.
💡 Struct tags are a Go language feature. CBOR tags relate to a CBOR data type (major type 6).
Struct tags for CBOR and JSON like `` `cbor:"name,omitempty"` `` and `` `json:"name,omitempty"` `` are supported so you can leverage your existing code. If both `cbor:` and `json:` tags exist then it will use `cbor:`.
New struct tags like __`keyasint`__ and __`toarray`__ make compact CBOR data such as COSE, CWT, and SenML easier to use.
⚓ [Quick Start](#quick-start) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Installation
👉 If Go modules aren't used, delete or modify example_test.go
from `"github.com/fxamacker/cbor/v2"` to `"github.com/fxamacker/cbor"`
Using Go modules is recommended.
```
$ GO111MODULE=on go get github.com/fxamacker/cbor/v2
```
```go
import (
"github.com/fxamacker/cbor/v2" // imports as package "cbor"
)
```
[Released versions](https://github.com/fxamacker/cbor/releases) benefit from longer fuzz tests.
## System Requirements
Using Go modules is recommended but not required.
* Go 1.12 (or newer).
* amd64, arm64, ppc64le and s390x. Other architectures may also work but they are not tested as frequently.
If Go modules feature isn't used, please see [Installation](#installation) about deleting or modifying example_test.go.
## Quick Start
🛡 Use Go's `io.LimitReader` to limit size when decoding very large or indefinite size data.
Functions with identical signatures to encoding/json include:
`Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, `decoder.Decode`.
__Default Mode__
If default options are acceptable, package level functions can be used for encoding and decoding.
```go
b, err := cbor.Marshal(v) // encode v to []byte b
err := cbor.Unmarshal(b, &v) // decode []byte b to v
encoder := cbor.NewEncoder(w) // create encoder with io.Writer w
decoder := cbor.NewDecoder(r) // create decoder with io.Reader r
```
__Modes__
If you need to use options or CBOR tags, then you'll want to create a mode.
"Mode" means defined way of encoding or decoding -- it links the standard API to your CBOR options and CBOR tags. This way, you don't pass around options and the API remains identical to `encoding/json`.
EncMode and DecMode are interfaces created from EncOptions or DecOptions structs.
For example, `em, err := cbor.EncOptions{...}.EncMode()` or `em, err := cbor.CanonicalEncOptions().EncMode()`.
EncMode and DecMode use immutable options so their behavior won't accidentally change at runtime. Modes are reusable, safe for concurrent use, and allow fast parallelism.
__Creating and Using Encoding Modes__
💡 Avoid using init(). For best performance, reuse EncMode and DecMode after creating them.
Most apps will probably create one EncMode and DecMode before init(). However, there's no limit and each can use different options.
```go
// Create EncOptions using either struct literal or a function.
opts := cbor.CanonicalEncOptions()
// If needed, modify opts. For example: opts.Time = cbor.TimeUnix
// Create reusable EncMode interface with immutable options, safe for concurrent use.
em, err := opts.EncMode()
// Use EncMode like encoding/json, with same function signatures.
b, err := em.Marshal(v) // encode v to []byte b
encoder := em.NewEncoder(w) // create encoder with io.Writer w
err := encoder.Encode(v) // encode v to io.Writer w
```
__Creating Modes With CBOR Tags__
A TagSet is used to specify CBOR tags.
```go
em, err := opts.EncMode() // no tags
em, err := opts.EncModeWithTags(ts) // immutable tags
em, err := opts.EncModeWithSharedTags(ts) // mutable shared tags
```
TagSet and all modes using it are safe for concurrent use. Equivalent API is available for DecMode.
__Predefined Encoding Options__
```go
func CanonicalEncOptions() EncOptions {} // settings for RFC 7049 Canonical CBOR
func CTAP2EncOptions() EncOptions {} // settings for FIDO2 CTAP2 Canonical CBOR
func CoreDetEncOptions() EncOptions {} // settings from a draft RFC (subject to change)
func PreferredUnsortedEncOptions() EncOptions {} // settings from a draft RFC (subject to change)
```
The empty curly braces prevent a syntax highlighting bug on GitHub, please ignore them.
__Struct Tags (keyasint, toarray, omitempty)__
The `keyasint`, `toarray`, and `omitempty` struct tags make it easy to use compact CBOR message formats. Internet standards often use CBOR arrays and CBOR maps with int keys to save space.
__More Info About API, Options, and Usage__
Options are listed in the Features section: [Encoding Options](#encoding-options) and [Decoding Options](#decoding-options)
For more details about each setting, see [Options](#options) section.
For additional API and usage examples, see [API](#api) and [Usage](#usage) sections.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Current Status
Latest version is v2.x, which has:
* __Stable API__ – Six codec function signatures will never change. No breaking API changes for other funcs in same major version. And these two functions are subject to change until the draft RFC is approved by IETF (est. in 2020):
* CoreDetEncOptions() is subject to change because it uses draft standard.
* PreferredUnsortedEncOptions() is subject to change because it uses draft standard.
* __Passed all tests__ – v2.x passed all 375+ tests on amd64, arm64, ppc64le and s390x with linux.
* __Passed fuzzing__ – v2.2 passed 459+ million execs in coverage-guided fuzzing on Feb 24, 2020 (still fuzzing.)
__Why v2.x?__:
v1 required breaking API changes to support new features like CBOR tags, detection of duplicate map keys, and having more functions with identical signatures to `encoding/json`.
v2.1 is roughly 26% faster and uses 57% fewer allocs than v1.x when decoding COSE and CWT using default options.
__Recent Activity__:
* Release v2.1 (Feb. 17, 2020)
- [x] CBOR tags (major type 6) for encoding and decoding.
- [x] Decoding options for duplicate map key detection: `DupMapKeyQuiet` (default) and `DupMapKeyEnforcedAPF`
- [x] Decoding optimizations. Structs using keyasint tag (like COSE and CWT) is
24-28% faster and 53-61% fewer allocs than both v1.5 and v2.0.1.
* Release v2.2 (Feb. 24, 2020)
- [x] CBOR BSTR <--> Go byte array (byte slices were already supported)
- [x] Add more encoding and decoding options (MaxNestedLevels, MaxArrayElements, MaxMapKeyPairs, TagsMd, etc.)
- [x] Fix potential error when decoding shorter CBOR indef length array to Go array (slice wasn't affected). This bug affects all prior versions of 1.x and 2.x.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Design Goals
This library is designed to be a generic CBOR encoder and decoder. It was initially created for a [WebAuthn (FIDO2) server library](https://github.com/fxamacker/webauthn), because existing CBOR libraries (in Go) didn't meet certain criteria in 2019.
This library is designed to be:
* __Easy__ – API is like `encoding/json` plus `keyasint` and `toarray` struct tags.
* __Small__ – Programs in cisco/senml are 4 MB smaller by switching to this library. In extreme cases programs can be smaller by 9+ MB. No code gen and the only imported pkg is x448/float16 which is maintained by the same team.
* __Safe and reliable__ – No `unsafe` pkg, coverage >95%, coverage-guided fuzzing, and data validation to avoid crashes on malformed or malicious data. Decoder settings include: `MaxNestedLevels`, `MaxArrayElements`, `MaxMapPairs`, and `IndefLength`.
Avoiding `unsafe` package has benefits. The `unsafe` package [warns](https://golang.org/pkg/unsafe/):
> Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.
All releases prioritize reliability to avoid crashes on decoding malformed CBOR data. See [Fuzzing and Coverage](#fuzzing-and-code-coverage).
Competing factors are balanced:
* __Speed__ vs __safety__ vs __size__ – to keep size small, avoid code generation. For safety, validate data and avoid Go's `unsafe` pkg. For speed, use safe optimizations such as caching struct metadata. This library is faster than a well-known library that uses `unsafe` and code gen.
* __Standards compliance__ vs __size__ – Supports CBOR RFC 7049 with minor [limitations](#limitations). To limit bloat, CBOR tags are supported but not all tags are built-in. The API allows users to add tags that aren't built-in. The API also allows custom encoding and decoding of user-defined Go types.
__Click to expand topic:__
<details>
<summary>Supported CBOR Features (Highlights)</summary><p>
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_features.svg?sanitize=1 "CBOR Features")
</details>
<details>
<summary>v2.0 API Design</summary><p>
v2.0 decoupled options from CBOR encoding & decoding functions:
* More encoding/decoding function signatures are identical to encoding/json.
* More function signatures can remain stable forever.
* More flexibility for evolving internal data types, optimizations, and concurrency.
* Features like CBOR tags can be added without more breaking API changes.
* Options to handle duplicate map keys can be added without more breaking API changes.
</details>
Features not in Go's standard library are usually not added. However, the __`toarray`__ struct tag in __ugorji/go__ was too useful to ignore. It was added in v1.3 when a project mentioned they were using it with CBOR to save disk space.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Features
### Standard API
Many function signatures are identical to encoding/json, including:
`Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, `decoder.Decode`.
`RawMessage` can be used to delay CBOR decoding or precompute CBOR encoding, like `encoding/json`.
Standard interfaces allow user-defined types to have custom CBOR encoding and decoding. They include:
`BinaryMarshaler`, `BinaryUnmarshaler`, `Marshaler`, and `Unmarshaler`.
`Marshaler` and `Unmarshaler` interfaces are satisfied by `MarshalCBOR` and `UnmarshalCBOR` functions using same params and return types as Go's MarshalJSON and UnmarshalJSON.
### Struct Tags
Support "cbor" and "json" keys in Go's struct tags. If both are specified, then "cbor" is used.
* `toarray` struct tag allows named struct fields for elements of CBOR arrays.
* `keyasint` struct tag allows named struct fields for elements of CBOR maps with int keys.
* `omitempty` struct tag excludes empty field values from being encoded.
See [Usage](#usage).
### CBOR Tags (New in v2.1)
There are three broad categories of CBOR tags:
* __Default built-in CBOR tags__ currently include tag numbers 0 and 1 (Time). Additional default built-in tags in future releases may include tag numbers 2 and 3 (Bignum).
* __Optional built-in CBOR tags__ may be provided in the future via build flags or optional package(s) to help reduce bloat.
* __User-defined CBOR tags__ are easy by using TagSet to associate tag numbers to user-defined Go types.
### Preferred Serialization
Preferred serialization encodes integers and floating-point values using the fewest bytes possible.
* Integers are always encoded using the fewest bytes possible.
* Floating-point values can optionally encode from float64->float32->float16 when values fit.
### Compact Data Size
The combination of preferred serialization and struct tags (toarray, keyasint, omitempty) allows very compact data size.
### Predefined Encoding Options
Easy-to-use functions (no params) return preset EncOptions struct:
`CanonicalEncOptions`, `CTAP2EncOptions`, `CoreDetEncOptions`, `PreferredUnsortedEncOptions`
### Encoding Options
Integers always encode to the shortest form that preserves value. By default, time values are encoded without tags.
Encoding of other data types and map key sort order are determined by encoder options.
| Encoding Option | Available Settings (defaults in bold, aliases in italics) |
| --------------- | --------------------------------------------------------- |
| EncOptions.Sort | __`SortNone`__, `SortLengthFirst`, `SortBytewiseLexical`, _`SortCanonical`_, _`SortCTAP2`_, _`SortCoreDeterministic`_ |
| EncOptions.Time | __`TimeUnix`__, `TimeUnixMicro`, `TimeUnixDynamic`, `TimeRFC3339`, `TimeRFC3339Nano` |
| EncOptions.TimeTag | __`EncTagNone`__, `EncTagRequired` |
| EncOptions.ShortestFloat | __`ShortestFloatNone`__, `ShortestFloat16` |
| EncOptions.InfConvert | __`InfConvertFloat16`__, `InfConvertNone` |
| EncOptions.NaNConvert | __`NaNConvert7e00`__, `NaNConvertNone`, `NaNConvertQuiet`, `NaNConvertPreserveSignal` |
| EncOptions.IndefLength | __`IndefLengthAllowed`__, `IndefLengthForbidden` |
| EncOptions.TagsMd | __`TagsAllowed`__, `TagsForbidden` |
See [Options](#options) section for details about each setting.
### Decoding Options
| Decoding Option | Available Settings (defaults in bold, aliases in italics) |
| --------------- | --------------------------------------------------------- |
| DecOptions.TimeTag | __`DecTagIgnored`__, `DecTagOptional`, `DecTagRequired` |
| DecOptions.DupMapKey | __`DupMapKeyQuiet`__, `DupMapKeyEnforcedAPF` |
| DecOptions.IndefLength | __`IndefLengthAllowed`__, `IndefLengthForbidden` |
| DecOptions.TagsMd | __`TagsAllowed`__, `TagsForbidden` |
| DecOptions.MaxNestedLevels | __32__, can be set to [4, 256] |
| DecOptions.MaxArrayElements | __131072__, can be set to [16, 134217728] |
| DecOptions.MaxMapPairs | __131072__, can be set to [16, 134217728] |
See [Options](#options) section for details about each setting.
### Additional Features
* Decoder always checks for invalid UTF-8 string errors.
* Decoder always decodes in-place to slices, maps, and structs.
* Decoder tries case-sensitive first and falls back to case-insensitive field name match when decoding to structs.
* Both encoder and decoder support indefinite length CBOR data (["streaming"](https://tools.ietf.org/html/rfc7049#section-2.2)).
* Both encoder and decoder correctly handles nil slice, map, pointer, and interface values.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Standards
This library is a full-featured generic CBOR [(RFC 7049)](https://tools.ietf.org/html/rfc7049) encoder and decoder. Notable CBOR features include:
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_features.svg?sanitize=1 "CBOR Features")
See the Features section for list of [Encoding Options](#encoding-options) and [Decoding Options](#decoding-options).
Known limitations are noted in the [Limitations section](#limitations).
Go nil values for slices, maps, pointers, etc. are encoded as CBOR null. Empty slices, maps, etc. are encoded as empty CBOR arrays and maps.
Decoder checks for all required well-formedness errors, including all "subkinds" of syntax errors and too little data.
After well-formedness is verified, basic validity errors are handled as follows:
* Invalid UTF-8 string: Decoder always checks and returns invalid UTF-8 string error.
* Duplicate keys in a map: Decoder has options to ignore or enforce rejection of duplicate map keys.
When decoding well-formed CBOR arrays and maps, decoder saves the first error it encounters and continues with the next item. Options to handle this differently may be added in the future.
See [Options](#options) section for detailed settings or [Features](#features) section for a summary of options.
__Click to expand topic:__
<details>
<summary>Duplicate Map Keys</summary><p>
This library provides options for fast detection and rejection of duplicate map keys based on applying a Go-specific data model to CBOR's extended generic data model in order to determine duplicate vs distinct map keys. Detection relies on whether the CBOR map key would be a duplicate "key" when decoded and applied to the user-provided Go map or struct.
`DupMapKeyQuiet` turns off detection of duplicate map keys. It tries to use a "keep fastest" method by choosing either "keep first" or "keep last" depending on the Go data type.
`DupMapKeyEnforcedAPF` enforces detection and rejection of duplidate map keys. Decoding stops immediately and returns `DupMapKeyError` when the first duplicate key is detected. The error includes the duplicate map key and the index number.
APF suffix means "Allow Partial Fill" so the destination map or struct can contain some decoded values at the time of error. It is the caller's responsibility to respond to the `DupMapKeyError` by discarding the partially filled result if that's required by their protocol.
</details>
## Limitations
If any of these limitations prevent you from using this library, please open an issue along with a link to your project.
* CBOR negative int (type 1) that cannot fit into Go's int64 are not supported, such as RFC 7049 example -18446744073709551616. Decoding these values returns `cbor.UnmarshalTypeError` like Go's `encoding/json`. However, this may be resolved in a future release by adding support for `big.Int`. Until then, users can use the API for custom encoding and decoding.
* CBOR `Undefined` (0xf7) value decodes to Go's `nil` value. CBOR `Null` (0xf6) more closely matches Go's `nil`.
* CBOR map keys with data types not supported by Go for map keys are ignored and an error is returned after continuing to decode remaining items.
* When using io.Reader interface to read very large or indefinite length CBOR data, Go's `io.LimitReader` should be used to limit size.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## API
Many function signatures are identical to Go's encoding/json, such as:
`Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, and `decoder.Decode`.
Interfaces identical or comparable to Go's encoding, encoding/json, or encoding/gob include:
`Marshaler`, `Unmarshaler`, `BinaryMarshaler`, and `BinaryUnmarshaler`.
Like `encoding/json`, `RawMessage` can be used to delay CBOR decoding or precompute CBOR encoding.
"Mode" in this API means defined way of encoding or decoding -- it links the standard API to CBOR options and CBOR tags.
EncMode and DecMode are interfaces created from EncOptions or DecOptions structs.
For example, `em, err := cbor.EncOptions{...}.EncMode()` or `em, err := cbor.CanonicalEncOptions().EncMode()`.
EncMode and DecMode use immutable options so their behavior won't accidentally change at runtime. Modes are intended to be reused and are safe for concurrent use.
__API for Default Mode__
If default options are acceptable, then you don't need to create EncMode or DecMode.
```go
Marshal(v interface{}) ([]byte, error)
NewEncoder(w io.Writer) *Encoder
Unmarshal(data []byte, v interface{}) error
NewDecoder(r io.Reader) *Decoder
```
__API for Creating & Using Encoding Modes__
```go
// EncMode interface uses immutable options and is safe for concurrent use.
type EncMode interface {
Marshal(v interface{}) ([]byte, error)
NewEncoder(w io.Writer) *Encoder
EncOptions() EncOptions // returns copy of options
}
// EncOptions specifies encoding options.
type EncOptions struct {
...
}
// EncMode returns an EncMode interface created from EncOptions.
func (opts EncOptions) EncMode() (EncMode, error) {}
// EncModeWithTags returns EncMode with options and tags that are both immutable.
func (opts EncOptions) EncModeWithTags(tags TagSet) (EncMode, error) {}
// EncModeWithSharedTags returns EncMode with immutable options and mutable shared tags.
func (opts EncOptions) EncModeWithSharedTags(tags TagSet) (EncMode, error) {}
```
The empty curly braces prevent a syntax highlighting bug, please ignore them.
__API for Predefined Encoding Options__
```go
func CanonicalEncOptions() EncOptions {} // settings for RFC 7049 Canonical CBOR
func CTAP2EncOptions() EncOptions {} // settings for FIDO2 CTAP2 Canonical CBOR
func CoreDetEncOptions() EncOptions {} // settings from a draft RFC (subject to change)
func PreferredUnsortedEncOptions() EncOptions {} // settings from a draft RFC (subject to change)
```
__API for Creating & Using Decoding Modes__
```go
// DecMode interface uses immutable options and is safe for concurrent use.
type DecMode interface {
Unmarshal(data []byte, v interface{}) error
NewDecoder(r io.Reader) *Decoder
DecOptions() DecOptions // returns copy of options
}
// DecOptions specifies decoding options.
type DecOptions struct {
...
}
// DecMode returns a DecMode interface created from DecOptions.
func (opts DecOptions) DecMode() (DecMode, error) {}
// DecModeWithTags returns DecMode with options and tags that are both immutable.
func (opts DecOptions) DecModeWithTags(tags TagSet) (DecMode, error) {}
// DecModeWithSharedTags returns DecMode with immutable options and mutable shared tags.
func (opts DecOptions) DecModeWithSharedTags(tags TagSet) (DecMode, error) {}
```
The empty curly braces prevent a syntax highlighting bug, please ignore them.
__API for Using CBOR Tags__
`TagSet` can be used to associate user-defined Go type(s) to tag number(s). It's also used to create EncMode or DecMode. For example, `em := EncOptions{...}.EncModeWithTags(ts)` or `em := EncOptions{...}.EncModeWithSharedTags(ts)`. This allows every standard API exported by em (like `Marshal` and `NewEncoder`) to use the specified tags automatically.
`Tag` and `RawTag` can be used to encode/decode a tag number with a Go value, but `TagSet` is generally recommended.
```go
type TagSet interface {
// Add adds given tag number(s), content type, and tag options to TagSet.
Add(opts TagOptions, contentType reflect.Type, num uint64, nestedNum ...uint64) error
// Remove removes given tag content type from TagSet.
Remove(contentType reflect.Type)
}
```
`Tag` and `RawTag` types can also be used to encode/decode tag number with Go value.
```go
type Tag struct {
Number uint64
Content interface{}
}
type RawTag struct {
Number uint64
Content RawMessage
}
```
See [API docs (godoc.org)](https://godoc.org/github.com/fxamacker/cbor) for more details and more functions. See [Usage section](#usage) for usage and code examples.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Options
Options for the decoding and encoding are listed here.
### Decoding Options
| DecOptions.TimeTag | Description |
| ------------------ | ----------- |
| DecTagIgnored (default) | Tag numbers are ignored (if present) for time values. |
| DecTagOptional | Tag numbers are only checked for validity if present for time values. |
| DecTagRequired | Tag numbers must be provided for time values except for CBOR Null and CBOR Undefined. |
CBOR Null and CBOR Undefined are silently treated as Go's zero time instant. Go's `time` package provides `IsZero` function, which reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC.
| DecOptions.DupMapKey | Description |
| -------------------- | ----------- |
| DupMapKeyQuiet (default) | turns off detection of duplicate map keys. It uses a "keep fastest" method by choosing either "keep first" or "keep last" depending on the Go data type. |
| DupMapKeyEnforcedAPF | enforces detection and rejection of duplidate map keys. Decoding stops immediately and returns `DupMapKeyError` when the first duplicate key is detected. The error includes the duplicate map key and the index number. |
`DupMapKeyEnforcedAPF` uses "Allow Partial Fill" so the destination map or struct can contain some decoded values at the time of error. Users can respond to the `DupMapKeyError` by discarding the partially filled result if that's required by their protocol.
| DecOptions.IndefLength | Description |
| ---------------------- | ----------- |
|IndefLengthAllowed (default) | allow indefinite length data |
|IndefLengthForbidden | forbid indefinite length data |
| DecOptions.TagsMd | Description |
| ----------------- | ----------- |
|TagsAllowed (default) | allow CBOR tags (major type 6) |
|TagsForbidden | forbid CBOR tags (major type 6) |
| DecOptions.MaxNestedLevels | Description |
| -------------------------- | ----------- |
| 32 (default) | allowed setting is [4, 256] |
| DecOptions.MaxArrayElements | Description |
| --------------------------- | ----------- |
| 131072 (default) | allowed setting is [16, 134217728] |
| DecOptions.MaxMapPairs | Description |
| ---------------------- | ----------- |
| 131072 (default) | allowed setting is [16, 134217728] |
### Encoding Options
__Integers always encode to the shortest form that preserves value__. Encoding of other data types and map key sort order are determined by encoding options.
These functions are provided to create and return a modifiable EncOptions struct with predefined settings.
| Predefined EncOptions | Description |
| --------------------- | ----------- |
| CanonicalEncOptions() |[Canonical CBOR (RFC 7049 Section 3.9)](https://tools.ietf.org/html/rfc7049#section-3.9). |
| CTAP2EncOptions() |[CTAP2 Canonical CBOR (FIDO2 CTAP2)](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#ctap2-canonical-cbor-encoding-form). |
| PreferredUnsortedEncOptions() |Unsorted, encode float64->float32->float16 when values fit, NaN values encoded as float16 0x7e00. |
| CoreDetEncOptions() |PreferredUnsortedEncOptions() + map keys are sorted bytewise lexicographic. |
🌱 CoreDetEncOptions() and PreferredUnsortedEncOptions() are subject to change until the draft RFC they used is approved by IETF.
| EncOptions.Sort | Description |
| --------------- | ----------- |
| SortNone (default) |No sorting for map keys. |
| SortLengthFirst |Length-first map key ordering. |
| SortBytewiseLexical |Bytewise lexicographic map key ordering |
| SortCanonical |(alias) Same as SortLengthFirst [(RFC 7049 Section 3.9)](https://tools.ietf.org/html/rfc7049#section-3.9) |
| SortCTAP2 |(alias) Same as SortBytewiseLexical [(CTAP2 Canonical CBOR)](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#ctap2-canonical-cbor-encoding-form). |
| SortCoreDeterministic |(alias) Same as SortBytewiseLexical. |
| EncOptions.Time | Description |
| --------------- | ----------- |
| TimeUnix (default) | (seconds) Encode as integer. |
| TimeUnixMicro | (microseconds) Encode as floating-point. ShortestFloat option determines size. |
| TimeUnixDynamic | (seconds or microseconds) Encode as integer if time doesn't have fractional seconds, otherwise encode as floating-point rounded to microseconds. |
| TimeRFC3339 | (seconds) Encode as RFC 3339 formatted string. |
| TimeRFC3339Nano | (nanoseconds) Encode as RFC3339 formatted string. |
| EncOptions.TimeTag | Description |
| ------------------ | ----------- |
| EncTagNone (default) | Tag number will not be encoded for time values. |
| EncTagRequired | Tag number (0 or 1) will be encoded unless time value is undefined/zero-instant. |
__Undefined Time Values__
By default, undefined (zero instant) time values will encode as CBOR Null without tag number for both EncTagNone and EncTagRequired. Although CBOR Undefined might be technically more correct for EncTagRequired, CBOR Undefined might not be supported by other generic decoders and it isn't supported by JSON.
Go's `time` package provides `IsZero` function, which reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC.
__Floating-Point Options__
Encoder has 3 types of options for floating-point data: ShortestFloatMode, InfConvertMode, and NaNConvertMode.
| EncOptions.ShortestFloat | Description |
| ------------------------ | ----------- |
| ShortestFloatNone (default) | No size conversion. Encode float32 and float64 to CBOR floating-point of same bit-size. |
| ShortestFloat16 | Encode float64 -> float32 -> float16 ([IEEE 754 binary16](https://en.wikipedia.org/wiki/Half-precision_floating-point_format)) when values fit. |
Conversions for infinity and NaN use InfConvert and NaNConvert settings.
| EncOptions.InfConvert | Description |
| --------------------- | ----------- |
| InfConvertFloat16 (default) | Convert +- infinity to float16 since they always preserve value (recommended) |
| InfConvertNone |Don't convert +- infinity to other representations -- used by CTAP2 Canonical CBOR |
| EncOptions.NaNConvert | Description |
| --------------------- | ----------- |
| NaNConvert7e00 (default) | Encode to 0xf97e00 (CBOR float16 = 0x7e00) -- used by RFC 7049 Canonical CBOR. |
| NaNConvertNone | Don't convert NaN to other representations -- used by CTAP2 Canonical CBOR. |
| NaNConvertQuiet | Force quiet bit = 1 and use shortest form that preserves NaN payload. |
| NaNConvertPreserveSignal | Convert to smallest form that preserves value (quit bit unmodified and NaN payload preserved). |
| EncOptions.IndefLength | Description |
| ---------------------- | ----------- |
|IndefLengthAllowed (default) | allow indefinite length data |
|IndefLengthForbidden | forbid indefinite length data |
| EncOptions.TagsMd | Description |
| ----------------- | ----------- |
|TagsAllowed (default) | allow CBOR tags (major type 6) |
|TagsForbidden | forbid CBOR tags (major type 6) |
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Usage
🛡 Use Go's `io.LimitReader` to limit size when decoding very large or indefinite size data.
Functions with identical signatures to encoding/json include:
`Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, `decoder.Decode`.
__Default Mode__
If default options are acceptable, package level functions can be used for encoding and decoding.
```go
b, err := cbor.Marshal(v) // encode v to []byte b
err := cbor.Unmarshal(b, &v) // decode []byte b to v
encoder := cbor.NewEncoder(w) // create encoder with io.Writer w
decoder := cbor.NewDecoder(r) // create decoder with io.Reader r
```
__Modes__
If you need to use options or CBOR tags, then you'll want to create a mode.
"Mode" means defined way of encoding or decoding -- it links the standard API to your CBOR options and CBOR tags. This way, you don't pass around options and the API remains identical to `encoding/json`.
EncMode and DecMode are interfaces created from EncOptions or DecOptions structs.
For example, `em, err := cbor.EncOptions{...}.EncMode()` or `em, err := cbor.CanonicalEncOptions().EncMode()`.
EncMode and DecMode use immutable options so their behavior won't accidentally change at runtime. Modes are reusable, safe for concurrent use, and allow fast parallelism.
__Creating and Using Encoding Modes__
EncMode is an interface ([API](#api)) created from EncOptions struct. EncMode uses immutable options after being created and is safe for concurrent use. For best performance, EncMode should be reused.
```go
// Create EncOptions using either struct literal or a function.
opts := cbor.CanonicalEncOptions()
// If needed, modify opts. For example: opts.Time = cbor.TimeUnix
// Create reusable EncMode interface with immutable options, safe for concurrent use.
em, err := opts.EncMode()
// Use EncMode like encoding/json, with same function signatures.
b, err := em.Marshal(v) // encode v to []byte b
encoder := em.NewEncoder(w) // create encoder with io.Writer w
err := encoder.Encode(v) // encode v to io.Writer w
```
__Struct Tags (keyasint, toarray, omitempty)__
The `keyasint`, `toarray`, and `omitempty` struct tags make it easy to use compact CBOR message formats. Internet standards often use CBOR arrays and CBOR maps with int keys to save space.
<hr>
[![CBOR API](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_api_struct_tags.png)](#usage)
<hr>
__Decoding CWT (CBOR Web Token)__ using `keyasint` and `toarray` struct tags:
```go
// Signed CWT is defined in RFC 8392
type signedCWT struct {
_ struct{} `cbor:",toarray"`
Protected []byte
Unprotected coseHeader
Payload []byte
Signature []byte
}
// Part of COSE header definition
type coseHeader struct {
Alg int `cbor:"1,keyasint,omitempty"`
Kid []byte `cbor:"4,keyasint,omitempty"`
IV []byte `cbor:"5,keyasint,omitempty"`
}
// data is []byte containing signed CWT
var v signedCWT
if err := cbor.Unmarshal(data, &v); err != nil {
return err
}
```
__Encoding CWT (CBOR Web Token)__ using `keyasint` and `toarray` struct tags:
```go
// Use signedCWT struct defined in "Decoding CWT" example.
var v signedCWT
...
if data, err := cbor.Marshal(v); err != nil {
return err
}
```
__Encoding and Decoding CWT (CBOR Web Token) with CBOR Tags__
```go
// Use signedCWT struct defined in "Decoding CWT" example.
// Create TagSet (safe for concurrency).
tags := cbor.NewTagSet()
// Register tag COSE_Sign1 18 with signedCWT type.
tags.Add(
cbor.TagOptions{EncTag: cbor.EncTagRequired, DecTag: cbor.DecTagRequired},
reflect.TypeOf(signedCWT{}),
18)
// Create DecMode with immutable tags.
dm, _ := cbor.DecOptions{}.DecModeWithTags(tags)
// Unmarshal to signedCWT with tag support.
var v signedCWT
if err := dm.Unmarshal(data, &v); err != nil {
return err
}
// Create EncMode with immutable tags.
em, _ := cbor.EncOptions{}.EncModeWithTags(tags)
// Marshal signedCWT with tag number.
if data, err := cbor.Marshal(v); err != nil {
return err
}
```
For more examples, see [examples_test.go](example_test.go).
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Comparisons
Comparisons are between this newer library and a well-known library that had 1,000+ stars before this library was created. Default build settings for each library were used for all comparisons.
__This library is safer__. Small malicious CBOR messages are rejected quickly before they exhaust system resources.
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_security_table.svg?sanitize=1 "CBOR Security Comparison")
__This library is smaller__. Programs like senmlCat can be 4 MB smaller by switching to this library. Programs using more complex CBOR data types can be 9.2 MB smaller.
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_size_comparison.png "CBOR library and program size comparison chart")
__This library is faster__ for encoding and decoding CBOR Web Token (CWT). However, speed is only one factor and it can vary depending on data types and sizes. Unlike the other library, this one doesn't use Go's ```unsafe``` package or code gen.
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_speed_comparison.png "CBOR library speed comparison chart")
The resource intensive `codec.CborHandle` initialization (in the other library) was placed outside the benchmark loop to make sure their library wasn't penalized.
__This library uses less memory__ for encoding and decoding CBOR Web Token (CWT) using test data from RFC 8392 A.1.
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.2.0/cbor_memory_table.svg?sanitize=1 "CBOR Speed Comparison")
Doing your own comparisons is highly recommended. Use your most common message sizes and data types.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Benchmarks
Go structs are faster than maps with string keys:
* decoding into struct is >28% faster than decoding into map.
* encoding struct is >35% faster than encoding map.
Go structs with `keyasint` struct tag are faster than maps with integer keys:
* decoding into struct is >28% faster than decoding into map.
* encoding struct is >34% faster than encoding map.
Go structs with `toarray` struct tag are faster than slice:
* decoding into struct is >15% faster than decoding into slice.
* encoding struct is >12% faster than encoding slice.
Doing your own benchmarks is highly recommended. Use your most common message sizes and data types.
See [Benchmarks for fxamacker/cbor](CBOR_BENCHMARKS.md).
## Fuzzing and Code Coverage
__Over 375 tests__ must pass on 4 architectures before tagging a release. They include all RFC 7049 examples, bugs found by fuzzing, maliciously crafted CBOR data, and over 87 tests with malformed data.
__Code coverage__ must not fall below 95% when tagging a release. Code coverage is 98.6% (`go test -cover`) for cbor v2.2 which is among the highest for libraries (in Go) of this type.
__Coverage-guided fuzzing__ must pass 250+ million execs before tagging a release. Fuzzing uses [fxamacker/cbor-fuzz](https://github.com/fxamacker/cbor-fuzz). Default corpus has:
* 2 files related to WebAuthn (FIDO U2F key).
* 3 files with custom struct.
* 9 files with [CWT examples (RFC 8392 Appendix A)](https://tools.ietf.org/html/rfc8392#appendix-A).
* 17 files with [COSE examples (RFC 8152 Appendix B & C)](https://github.com/cose-wg/Examples/tree/master/RFC8152).
* 81 files with [CBOR examples (RFC 7049 Appendix A) ](https://tools.ietf.org/html/rfc7049#appendix-A). It excludes 1 errata first reported in [issue #46](https://github.com/fxamacker/cbor/issues/46).
Over 1,100 files (corpus) are used for fuzzing because it includes fuzz-generated corpus.
To prevent excessive delays, fuzzing is not restarted for a release if changes are limited to docs and comments.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)
## Versions and API Changes
This project uses [Semantic Versioning](https://semver.org), so the API is always backwards compatible unless the major version number changes.
These functions have signatures identical to encoding/json and they will likely never change even after major new releases: `Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder`, `encoder.Encode`, and `decoder.Decode`.
Newly added API documented as "subject to change" are excluded from SemVer.
Newly added API in the master branch that has never been release tagged are excluded from SemVer.
## Code of Conduct
This project has adopted the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). Contact [faye.github@gmail.com](mailto:faye.github@gmail.com) with any questions or comments.
## Contributing
Please refer to [How to Contribute](CONTRIBUTING.md).
## Security Policy
Security fixes are provided for the latest released version.
To report security vulnerabilities, please email [faye.github@gmail.com](mailto:faye.github@gmail.com) and allow time for the problem to be resolved before reporting it to the public.
## Disclaimers
Phrases like "no crashes" or "doesn't crash" mean there are no known crash bugs in the latest version based on results of unit tests and coverage-guided fuzzing. It doesn't imply the software is 100% bug-free or 100% invulnerable to all known and unknown attacks.
Please read the license for additional disclaimers and terms.
## Special Thanks
__Making this library better__
* Montgomery Edwards⁴⁴⁸ for [x448/float16](https://github.com/x448/float16), updating the docs, creating charts & slideshow, filing issues, nudging me to ask for feedback from users, helping with design of v2.0-v2.1 API, and general idea for DupMapKeyEnforcedAPF.
* Stefan Tatschner for using this library in [sep](https://git.sr.ht/~rumpelsepp/sep), being the 1st to discover my CBOR library, requesting time.Time in issue #1, and submitting this library in a [PR to cbor.io](https://github.com/cbor/cbor.github.io/pull/56) on Aug 12, 2019.
* Yawning Angel for using this library to [oasis-core](https://github.com/oasislabs/oasis-core), and requesting BinaryMarshaler in issue #5.
* Jernej Kos for requesting RawMessage in issue #11 and offering feedback on v2.1 API for CBOR tags.
* ZenGround0 for using this library in [go-filecoin](https://github.com/filecoin-project/go-filecoin), filing "toarray" bug in issue #129, and requesting
CBOR BSTR <--> Go array in #133.
* Keith Randall for [fixing Go bugs and providing workarounds](https://github.com/golang/go/issues/36400) so we don't have to wait for new versions of Go.
__Help clarifying CBOR RFC 7049 or 7049bis__
* Carsten Bormann for RFC 7049 (CBOR), his fast confirmation to my RFC 7049 errata, approving my pull request to 7049bis, and his patience when I misread a line in 7049bis.
* Laurence Lundblade for his help on the IETF mailing list for 7049bis and for pointing out on a CBORbis issue that CBOR Undefined might be problematic translating to JSON.
* Jeffrey Yasskin for his help on the IETF mailing list for 7049bis.
__Words of encouragement and support__
* Jakob Borg for his words of encouragement about this library at Go Forum. This is especially appreciated in the early stages when there's a lot of rough edges.
## License
Copyright © 2019-present [Faye Amacker](https://github.com/fxamacker).
fxamacker/cbor is licensed under the MIT License. See [LICENSE](LICENSE) for the full license text.
<hr>
⚓ [Install](#installation) • [Status](#current-status) • [Design Goals](#design-goals) • [Features](#features) • [Standards](#standards) • [API](#api) • [Usage](#usage) • [Fuzzing](#fuzzing-and-code-coverage) • [Security Policy](#security-policy) • [License](#license)

@ -0,0 +1,308 @@
// Copyright (c) Faye Amacker. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
package cbor
import (
"bytes"
"errors"
"reflect"
"sort"
"strconv"
"strings"
"sync"
)
var (
decodingStructTypeCache sync.Map // map[reflect.Type]*decodingStructType
encodingStructTypeCache sync.Map // map[reflect.Type]*encodingStructType
encodeFuncCache sync.Map // map[reflect.Type]encodeFunc
typeInfoCache sync.Map // map[reflect.Type]*typeInfo
)
type specialType int
const (
specialTypeNone specialType = iota
specialTypeUnmarshalerIface
specialTypeEmptyIface
specialTypeTag
specialTypeTime
)
type typeInfo struct {
elemTypeInfo *typeInfo
keyTypeInfo *typeInfo
typ reflect.Type
kind reflect.Kind
nonPtrType reflect.Type
nonPtrKind reflect.Kind
spclType specialType
}
func newTypeInfo(t reflect.Type) *typeInfo {
tInfo := typeInfo{typ: t, kind: t.Kind()}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
k := t.Kind()
tInfo.nonPtrType = t
tInfo.nonPtrKind = k
if k == reflect.Interface && t.NumMethod() == 0 {
tInfo.spclType = specialTypeEmptyIface
} else if t == typeTag {
tInfo.spclType = specialTypeTag
} else if t == typeTime {
tInfo.spclType = specialTypeTime
} else if reflect.PtrTo(t).Implements(typeUnmarshaler) {
tInfo.spclType = specialTypeUnmarshalerIface
}
switch k {
case reflect.Array, reflect.Slice:
tInfo.elemTypeInfo = getTypeInfo(t.Elem())
case reflect.Map:
tInfo.keyTypeInfo = getTypeInfo(t.Key())
tInfo.elemTypeInfo = getTypeInfo(t.Elem())
}
return &tInfo
}
type decodingStructType struct {
fields fields
err error
toArray bool
}
func getDecodingStructType(t reflect.Type) *decodingStructType {
if v, _ := decodingStructTypeCache.Load(t); v != nil {
return v.(*decodingStructType)
}
flds, structOptions := getFields(t)
toArray := hasToArrayOption(structOptions)
var err error
for i := 0; i < len(flds); i++ {
if flds[i].keyAsInt {
nameAsInt, numErr := strconv.Atoi(flds[i].name)
if numErr != nil {
err = errors.New("cbor: failed to parse field name \"" + flds[i].name + "\" to int (" + numErr.Error() + ")")
break
}
flds[i].nameAsInt = int64(nameAsInt)
}
flds[i].typInfo = getTypeInfo(flds[i].typ)
}
structType := &decodingStructType{fields: flds, err: err, toArray: toArray}
decodingStructTypeCache.Store(t, structType)
return structType
}
type encodingStructType struct {
fields fields
bytewiseFields fields
lengthFirstFields fields
err error
toArray bool
omitEmpty bool
hasAnonymousField bool
}
func (st *encodingStructType) getFields(em *encMode) fields {
if em.sort == SortNone {
return st.fields
}
if em.sort == SortLengthFirst {
return st.lengthFirstFields
}
return st.bytewiseFields
}
type bytewiseFieldSorter struct {
fields fields
}
func (x *bytewiseFieldSorter) Len() int {
return len(x.fields)
}
func (x *bytewiseFieldSorter) Swap(i, j int) {
x.fields[i], x.fields[j] = x.fields[j], x.fields[i]
}
func (x *bytewiseFieldSorter) Less(i, j int) bool {
return bytes.Compare(x.fields[i].cborName, x.fields[j].cborName) <= 0
}
type lengthFirstFieldSorter struct {
fields fields
}
func (x *lengthFirstFieldSorter) Len() int {
return len(x.fields)
}
func (x *lengthFirstFieldSorter) Swap(i, j int) {
x.fields[i], x.fields[j] = x.fields[j], x.fields[i]
}
func (x *lengthFirstFieldSorter) Less(i, j int) bool {
if len(x.fields[i].cborName) != len(x.fields[j].cborName) {
return len(x.fields[i].cborName) < len(x.fields[j].cborName)
}
return bytes.Compare(x.fields[i].cborName, x.fields[j].cborName) <= 0
}
func getEncodingStructType(t reflect.Type) *encodingStructType {
if v, _ := encodingStructTypeCache.Load(t); v != nil {
return v.(*encodingStructType)
}
flds, structOptions := getFields(t)
if hasToArrayOption(structOptions) {
return getEncodingStructToArrayType(t, flds)
}
var err error
var omitEmpty bool
var hasAnonymousField bool
var hasKeyAsInt bool
var hasKeyAsStr bool
e := getEncodeState()
for i := 0; i < len(flds); i++ {
// Get field's encodeFunc
flds[i].ef = getEncodeFunc(flds[i].typ)
if flds[i].ef == nil {
err = &UnsupportedTypeError{t}
break
}
// Encode field name
if flds[i].keyAsInt {
nameAsInt, numErr := strconv.Atoi(flds[i].name)
if numErr != nil {
err = errors.New("cbor: failed to parse field name \"" + flds[i].name + "\" to int (" + numErr.Error() + ")")
break
}
flds[i].nameAsInt = int64(nameAsInt)
if nameAsInt >= 0 {
encodeHead(e, byte(cborTypePositiveInt), uint64(nameAsInt))
} else {
n := nameAsInt*(-1) - 1
encodeHead(e, byte(cborTypeNegativeInt), uint64(n))
}
flds[i].cborName = make([]byte, e.Len())
copy(flds[i].cborName, e.Bytes())
e.Reset()
hasKeyAsInt = true
} else {
encodeHead(e, byte(cborTypeTextString), uint64(len(flds[i].name)))
flds[i].cborName = make([]byte, e.Len()+len(flds[i].name))
n := copy(flds[i].cborName, e.Bytes())
copy(flds[i].cborName[n:], flds[i].name)
e.Reset()
hasKeyAsStr = true
}
// Check if field is from embedded struct
if len(flds[i].idx) > 1 {
hasAnonymousField = true
}
// Check if field can be omitted when empty
if flds[i].omitEmpty {
omitEmpty = true
}
}
putEncodeState(e)
if err != nil {
structType := &encodingStructType{err: err}
encodingStructTypeCache.Store(t, structType)
return structType
}
// Sort fields by canonical order
bytewiseFields := make(fields, len(flds))
copy(bytewiseFields, flds)
sort.Sort(&bytewiseFieldSorter{bytewiseFields})
lengthFirstFields := bytewiseFields
if hasKeyAsInt && hasKeyAsStr {
lengthFirstFields = make(fields, len(flds))
copy(lengthFirstFields, flds)
sort.Sort(&lengthFirstFieldSorter{lengthFirstFields})
}
structType := &encodingStructType{
fields: flds,
bytewiseFields: bytewiseFields,
lengthFirstFields: lengthFirstFields,
omitEmpty: omitEmpty,
hasAnonymousField: hasAnonymousField,
}
encodingStructTypeCache.Store(t, structType)
return structType
}
func getEncodingStructToArrayType(t reflect.Type, flds fields) *encodingStructType {
var hasAnonymousField bool
for i := 0; i < len(flds); i++ {
// Get field's encodeFunc
flds[i].ef = getEncodeFunc(flds[i].typ)
if flds[i].ef == nil {
structType := &encodingStructType{err: &UnsupportedTypeError{t}}
encodingStructTypeCache.Store(t, structType)
return structType
}
// Check if field is from embedded struct
if len(flds[i].idx) > 1 {
hasAnonymousField = true
}
}
structType := &encodingStructType{
fields: flds,
toArray: true,
hasAnonymousField: hasAnonymousField,
}
encodingStructTypeCache.Store(t, structType)
return structType
}
func getEncodeFunc(t reflect.Type) encodeFunc {
if v, _ := encodeFuncCache.Load(t); v != nil {
return v.(encodeFunc)
}
f := getEncodeFuncInternal(t)
encodeFuncCache.Store(t, f)
return f
}
func getTypeInfo(t reflect.Type) *typeInfo {
if v, _ := typeInfoCache.Load(t); v != nil {
return v.(*typeInfo)
}
tInfo := newTypeInfo(t)
typeInfoCache.Store(t, tInfo)
return tInfo
}
func hasToArrayOption(tag string) bool {
s := ",toarray"
idx := strings.Index(tag, s)
return idx >= 0 && (len(tag) == idx+len(s) || tag[idx+len(s)] == ',')
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save