fix: address outdated dependencies for go-git CVE upgrade (#3440)
This commit is contained in:
parent
16c43bb973
commit
3cf227e8af
20
go.mod
20
go.mod
|
|
@ -13,7 +13,7 @@ require (
|
||||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589
|
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589
|
||||||
github.com/containerd/cgroups v1.1.0 // indirect
|
github.com/containerd/cgroups v1.1.0 // indirect
|
||||||
github.com/docker/docker v27.0.3+incompatible
|
github.com/docker/docker v27.0.3+incompatible
|
||||||
github.com/go-git/go-billy/v5 v5.5.0
|
github.com/go-git/go-billy/v5 v5.6.1
|
||||||
github.com/go-git/go-git/v5 v5.13.1
|
github.com/go-git/go-git/v5 v5.13.1
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-cmp v0.6.0
|
github.com/google/go-cmp v0.6.0
|
||||||
|
|
@ -29,9 +29,9 @@ require (
|
||||||
github.com/spf13/afero v1.11.0
|
github.com/spf13/afero v1.11.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.33.0
|
||||||
golang.org/x/oauth2 v0.21.0
|
golang.org/x/oauth2 v0.21.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -55,7 +55,7 @@ require (
|
||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
github.com/ProtonMail/go-crypto v1.1.3 // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||||
|
|
@ -126,9 +126,9 @@ require (
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
go.etcd.io/etcd/raft/v3 v3.5.6 // indirect
|
go.etcd.io/etcd/raft/v3 v3.5.6 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
golang.org/x/sys v0.22.0
|
golang.org/x/sys v0.28.0
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/api v0.188.0
|
google.golang.org/api v0.188.0
|
||||||
google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b // indirect
|
google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||||
|
|
@ -156,7 +156,7 @@ require (
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/containerd/ttrpc v1.2.5 // indirect
|
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
|
@ -175,7 +175,7 @@ require (
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/spf13/viper v1.18.2 // indirect
|
github.com/spf13/viper v1.18.2 // indirect
|
||||||
|
|
@ -187,7 +187,7 @@ require (
|
||||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
|
|
||||||
83
go.sum
83
go.sum
|
|
@ -66,8 +66,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
||||||
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
||||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
||||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
|
@ -124,7 +124,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
|
@ -135,7 +134,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4=
|
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4=
|
||||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM=
|
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM=
|
||||||
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/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||||
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=
|
||||||
|
|
@ -168,8 +166,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
|
@ -198,8 +196,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/ePirat/docker-credential-gitlabci v1.0.0 h1:YRkUSvkON6rT88vtscClAmPEYWhtltGEAuRVYtz1/+Y=
|
github.com/ePirat/docker-credential-gitlabci v1.0.0 h1:YRkUSvkON6rT88vtscClAmPEYWhtltGEAuRVYtz1/+Y=
|
||||||
github.com/ePirat/docker-credential-gitlabci v1.0.0/go.mod h1:Ptmh+D0lzBQtgb6+QHjXl9HqOn3T1P8fKUHldiSQQGA=
|
github.com/ePirat/docker-credential-gitlabci v1.0.0/go.mod h1:Ptmh+D0lzBQtgb6+QHjXl9HqOn3T1P8fKUHldiSQQGA=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
|
@ -213,16 +211,16 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
|
||||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
|
@ -384,8 +382,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
|
@ -430,8 +428,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
||||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
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/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
|
|
@ -443,8 +441,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
|
@ -471,8 +469,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||||
|
|
@ -521,14 +520,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
|
@ -536,7 +533,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
|
@ -552,11 +548,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
|
@ -568,9 +562,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|
@ -592,29 +585,22 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
@ -629,7 +615,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ import (
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/ProtonMail/go-crypto/internal/byteutil"
|
|
||||||
"math/bits"
|
"math/bits"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/internal/byteutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ocb struct {
|
type ocb struct {
|
||||||
|
|
@ -153,7 +154,7 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
|
||||||
truncatedNonce := make([]byte, len(nonce))
|
truncatedNonce := make([]byte, len(nonce))
|
||||||
copy(truncatedNonce, nonce)
|
copy(truncatedNonce, nonce)
|
||||||
truncatedNonce[len(truncatedNonce)-1] &= 192
|
truncatedNonce[len(truncatedNonce)-1] &= 192
|
||||||
Ktop := make([]byte, blockSize)
|
var Ktop []byte
|
||||||
if bytes.Equal(truncatedNonce, o.reusableKtop.noncePrefix) {
|
if bytes.Equal(truncatedNonce, o.reusableKtop.noncePrefix) {
|
||||||
Ktop = o.reusableKtop.Ktop
|
Ktop = o.reusableKtop.Ktop
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import (
|
||||||
// Headers
|
// Headers
|
||||||
//
|
//
|
||||||
// base64-encoded Bytes
|
// base64-encoded Bytes
|
||||||
// '=' base64 encoded checksum
|
// '=' base64 encoded checksum (optional) not checked anymore
|
||||||
// -----END Type-----
|
// -----END Type-----
|
||||||
//
|
//
|
||||||
// where Headers is a possibly empty sequence of Key: Value lines.
|
// where Headers is a possibly empty sequence of Key: Value lines.
|
||||||
|
|
@ -40,36 +40,15 @@ type Block struct {
|
||||||
|
|
||||||
var ArmorCorrupt error = errors.StructuralError("armor invalid")
|
var ArmorCorrupt error = errors.StructuralError("armor invalid")
|
||||||
|
|
||||||
const crc24Init = 0xb704ce
|
|
||||||
const crc24Poly = 0x1864cfb
|
|
||||||
const crc24Mask = 0xffffff
|
|
||||||
|
|
||||||
// crc24 calculates the OpenPGP checksum as specified in RFC 4880, section 6.1
|
|
||||||
func crc24(crc uint32, d []byte) uint32 {
|
|
||||||
for _, b := range d {
|
|
||||||
crc ^= uint32(b) << 16
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
crc <<= 1
|
|
||||||
if crc&0x1000000 != 0 {
|
|
||||||
crc ^= crc24Poly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return crc
|
|
||||||
}
|
|
||||||
|
|
||||||
var armorStart = []byte("-----BEGIN ")
|
var armorStart = []byte("-----BEGIN ")
|
||||||
var armorEnd = []byte("-----END ")
|
var armorEnd = []byte("-----END ")
|
||||||
var armorEndOfLine = []byte("-----")
|
var armorEndOfLine = []byte("-----")
|
||||||
|
|
||||||
// lineReader wraps a line based reader. It watches for the end of an armor
|
// lineReader wraps a line based reader. It watches for the end of an armor block
|
||||||
// block and records the expected CRC value.
|
|
||||||
type lineReader struct {
|
type lineReader struct {
|
||||||
in *bufio.Reader
|
in *bufio.Reader
|
||||||
buf []byte
|
buf []byte
|
||||||
eof bool
|
eof bool
|
||||||
crc uint32
|
|
||||||
crcSet bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lineReader) Read(p []byte) (n int, err error) {
|
func (l *lineReader) Read(p []byte) (n int, err error) {
|
||||||
|
|
@ -98,26 +77,9 @@ func (l *lineReader) Read(p []byte) (n int, err error) {
|
||||||
|
|
||||||
if len(line) == 5 && line[0] == '=' {
|
if len(line) == 5 && line[0] == '=' {
|
||||||
// This is the checksum line
|
// This is the checksum line
|
||||||
var expectedBytes [3]byte
|
// Don't check the checksum
|
||||||
var m int
|
|
||||||
m, err = base64.StdEncoding.Decode(expectedBytes[0:], line[1:])
|
|
||||||
if m != 3 || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.crc = uint32(expectedBytes[0])<<16 |
|
|
||||||
uint32(expectedBytes[1])<<8 |
|
|
||||||
uint32(expectedBytes[2])
|
|
||||||
|
|
||||||
line, _, err = l.in.ReadLine()
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !bytes.HasPrefix(line, armorEnd) {
|
|
||||||
return 0, ArmorCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
l.eof = true
|
l.eof = true
|
||||||
l.crcSet = true
|
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,23 +100,14 @@ func (l *lineReader) Read(p []byte) (n int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// openpgpReader passes Read calls to the underlying base64 decoder, but keeps
|
// openpgpReader passes Read calls to the underlying base64 decoder.
|
||||||
// a running CRC of the resulting data and checks the CRC against the value
|
|
||||||
// found by the lineReader at EOF.
|
|
||||||
type openpgpReader struct {
|
type openpgpReader struct {
|
||||||
lReader *lineReader
|
lReader *lineReader
|
||||||
b64Reader io.Reader
|
b64Reader io.Reader
|
||||||
currentCRC uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *openpgpReader) Read(p []byte) (n int, err error) {
|
func (r *openpgpReader) Read(p []byte) (n int, err error) {
|
||||||
n, err = r.b64Reader.Read(p)
|
n, err = r.b64Reader.Read(p)
|
||||||
r.currentCRC = crc24(r.currentCRC, p[:n])
|
|
||||||
|
|
||||||
if err == io.EOF && r.lReader.crcSet && r.lReader.crc != uint32(r.currentCRC&crc24Mask) {
|
|
||||||
return 0, ArmorCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +175,6 @@ TryNextBlock:
|
||||||
}
|
}
|
||||||
|
|
||||||
p.lReader.in = r
|
p.lReader.in = r
|
||||||
p.oReader.currentCRC = crc24Init
|
|
||||||
p.oReader.lReader = &p.lReader
|
p.oReader.lReader = &p.lReader
|
||||||
p.oReader.b64Reader = base64.NewDecoder(base64.StdEncoding, &p.lReader)
|
p.oReader.b64Reader = base64.NewDecoder(base64.StdEncoding, &p.lReader)
|
||||||
p.Body = &p.oReader
|
p.Body = &p.oReader
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,23 @@ var blockEnd = []byte("\n=")
|
||||||
var newline = []byte("\n")
|
var newline = []byte("\n")
|
||||||
var armorEndOfLineOut = []byte("-----\n")
|
var armorEndOfLineOut = []byte("-----\n")
|
||||||
|
|
||||||
|
const crc24Init = 0xb704ce
|
||||||
|
const crc24Poly = 0x1864cfb
|
||||||
|
|
||||||
|
// crc24 calculates the OpenPGP checksum as specified in RFC 4880, section 6.1
|
||||||
|
func crc24(crc uint32, d []byte) uint32 {
|
||||||
|
for _, b := range d {
|
||||||
|
crc ^= uint32(b) << 16
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
crc <<= 1
|
||||||
|
if crc&0x1000000 != 0 {
|
||||||
|
crc ^= crc24Poly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc
|
||||||
|
}
|
||||||
|
|
||||||
// writeSlices writes its arguments to the given Writer.
|
// writeSlices writes its arguments to the given Writer.
|
||||||
func writeSlices(out io.Writer, slices ...[]byte) (err error) {
|
func writeSlices(out io.Writer, slices ...[]byte) (err error) {
|
||||||
for _, s := range slices {
|
for _, s := range slices {
|
||||||
|
|
@ -99,15 +116,18 @@ func (l *lineBreaker) Close() (err error) {
|
||||||
//
|
//
|
||||||
// encoding -> base64 encoder -> lineBreaker -> out
|
// encoding -> base64 encoder -> lineBreaker -> out
|
||||||
type encoding struct {
|
type encoding struct {
|
||||||
out io.Writer
|
out io.Writer
|
||||||
breaker *lineBreaker
|
breaker *lineBreaker
|
||||||
b64 io.WriteCloser
|
b64 io.WriteCloser
|
||||||
crc uint32
|
crc uint32
|
||||||
blockType []byte
|
crcEnabled bool
|
||||||
|
blockType []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoding) Write(data []byte) (n int, err error) {
|
func (e *encoding) Write(data []byte) (n int, err error) {
|
||||||
e.crc = crc24(e.crc, data)
|
if e.crcEnabled {
|
||||||
|
e.crc = crc24(e.crc, data)
|
||||||
|
}
|
||||||
return e.b64.Write(data)
|
return e.b64.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,20 +138,21 @@ func (e *encoding) Close() (err error) {
|
||||||
}
|
}
|
||||||
e.breaker.Close()
|
e.breaker.Close()
|
||||||
|
|
||||||
var checksumBytes [3]byte
|
if e.crcEnabled {
|
||||||
checksumBytes[0] = byte(e.crc >> 16)
|
var checksumBytes [3]byte
|
||||||
checksumBytes[1] = byte(e.crc >> 8)
|
checksumBytes[0] = byte(e.crc >> 16)
|
||||||
checksumBytes[2] = byte(e.crc)
|
checksumBytes[1] = byte(e.crc >> 8)
|
||||||
|
checksumBytes[2] = byte(e.crc)
|
||||||
|
|
||||||
var b64ChecksumBytes [4]byte
|
var b64ChecksumBytes [4]byte
|
||||||
base64.StdEncoding.Encode(b64ChecksumBytes[:], checksumBytes[:])
|
base64.StdEncoding.Encode(b64ChecksumBytes[:], checksumBytes[:])
|
||||||
|
|
||||||
return writeSlices(e.out, blockEnd, b64ChecksumBytes[:], newline, armorEnd, e.blockType, armorEndOfLine)
|
return writeSlices(e.out, blockEnd, b64ChecksumBytes[:], newline, armorEnd, e.blockType, armorEndOfLine)
|
||||||
|
}
|
||||||
|
return writeSlices(e.out, newline, armorEnd, e.blockType, armorEndOfLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode returns a WriteCloser which will encode the data written to it in
|
func encode(out io.Writer, blockType string, headers map[string]string, checksum bool) (w io.WriteCloser, err error) {
|
||||||
// OpenPGP armor.
|
|
||||||
func Encode(out io.Writer, blockType string, headers map[string]string) (w io.WriteCloser, err error) {
|
|
||||||
bType := []byte(blockType)
|
bType := []byte(blockType)
|
||||||
err = writeSlices(out, armorStart, bType, armorEndOfLineOut)
|
err = writeSlices(out, armorStart, bType, armorEndOfLineOut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -151,11 +172,27 @@ func Encode(out io.Writer, blockType string, headers map[string]string) (w io.Wr
|
||||||
}
|
}
|
||||||
|
|
||||||
e := &encoding{
|
e := &encoding{
|
||||||
out: out,
|
out: out,
|
||||||
breaker: newLineBreaker(out, 64),
|
breaker: newLineBreaker(out, 64),
|
||||||
crc: crc24Init,
|
blockType: bType,
|
||||||
blockType: bType,
|
crc: crc24Init,
|
||||||
|
crcEnabled: checksum,
|
||||||
}
|
}
|
||||||
e.b64 = base64.NewEncoder(base64.StdEncoding, e.breaker)
|
e.b64 = base64.NewEncoder(base64.StdEncoding, e.breaker)
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode returns a WriteCloser which will encode the data written to it in
|
||||||
|
// OpenPGP armor.
|
||||||
|
func Encode(out io.Writer, blockType string, headers map[string]string) (w io.WriteCloser, err error) {
|
||||||
|
return encode(out, blockType, headers, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeWithChecksumOption returns a WriteCloser which will encode the data written to it in
|
||||||
|
// OpenPGP armor and provides the option to include a checksum.
|
||||||
|
// When forming ASCII Armor, the CRC24 footer SHOULD NOT be generated,
|
||||||
|
// unless interoperability with implementations that require the CRC24 footer
|
||||||
|
// to be present is a concern.
|
||||||
|
func EncodeWithChecksumOption(out io.Writer, blockType string, headers map[string]string, doChecksum bool) (w io.WriteCloser, err error) {
|
||||||
|
return encode(out, blockType, headers, doChecksum)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,12 @@ func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) {
|
||||||
if c == '\r' {
|
if c == '\r' {
|
||||||
*s = 1
|
*s = 1
|
||||||
} else if c == '\n' {
|
} else if c == '\n' {
|
||||||
cw.Write(buf[start:i])
|
if _, err := cw.Write(buf[start:i]); err != nil {
|
||||||
cw.Write(newline)
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := cw.Write(newline); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
start = i + 1
|
start = i + 1
|
||||||
}
|
}
|
||||||
case 1:
|
case 1:
|
||||||
|
|
@ -39,7 +43,9 @@ func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cw.Write(buf[start:])
|
if _, err := cw.Write(buf[start:]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
return len(buf), nil
|
return len(buf), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,13 +163,9 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead
|
||||||
if _, err := param.Write([]byte("Anonymous Sender ")); err != nil {
|
if _, err := param.Write([]byte("Anonymous Sender ")); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// For v5 keys, the 20 leftmost octets of the fingerprint are used.
|
if _, err := param.Write(fingerprint[:]); err != nil {
|
||||||
if _, err := param.Write(fingerprint[:20]); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if param.Len()-len(curveOID) != 45 {
|
|
||||||
return nil, errors.New("ecdh: malformed KDF Param")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param );
|
// MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param );
|
||||||
h := pub.KDF.Hash.New()
|
h := pub.KDF.Hash.New()
|
||||||
|
|
|
||||||
115
vendor/github.com/ProtonMail/go-crypto/openpgp/ed25519/ed25519.go
generated
vendored
Normal file
115
vendor/github.com/ProtonMail/go-crypto/openpgp/ed25519/ed25519.go
generated
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Package ed25519 implements the ed25519 signature algorithm for OpenPGP
|
||||||
|
// as defined in the Open PGP crypto refresh.
|
||||||
|
package ed25519
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
ed25519lib "github.com/cloudflare/circl/sign/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicKeySize is the size, in bytes, of public keys in this package.
|
||||||
|
PublicKeySize = ed25519lib.PublicKeySize
|
||||||
|
// SeedSize is the size, in bytes, of private key seeds.
|
||||||
|
// The private key representation used by RFC 8032.
|
||||||
|
SeedSize = ed25519lib.SeedSize
|
||||||
|
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
|
||||||
|
SignatureSize = ed25519lib.SignatureSize
|
||||||
|
)
|
||||||
|
|
||||||
|
type PublicKey struct {
|
||||||
|
// Point represents the elliptic curve point of the public key.
|
||||||
|
Point []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrivateKey struct {
|
||||||
|
PublicKey
|
||||||
|
// Key the private key representation by RFC 8032,
|
||||||
|
// encoded as seed | pub key point.
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey creates a new empty ed25519 public key.
|
||||||
|
func NewPublicKey() *PublicKey {
|
||||||
|
return &PublicKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey creates a new empty private key referencing the public key.
|
||||||
|
func NewPrivateKey(key PublicKey) *PrivateKey {
|
||||||
|
return &PrivateKey{
|
||||||
|
PublicKey: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed returns the ed25519 private key secret seed.
|
||||||
|
// The private key representation by RFC 8032.
|
||||||
|
func (pk *PrivateKey) Seed() []byte {
|
||||||
|
return pk.Key[:SeedSize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalByteSecret returns the underlying 32 byte seed of the private key.
|
||||||
|
func (pk *PrivateKey) MarshalByteSecret() []byte {
|
||||||
|
return pk.Seed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalByteSecret computes the private key from the secret seed
|
||||||
|
// and stores it in the private key object.
|
||||||
|
func (sk *PrivateKey) UnmarshalByteSecret(seed []byte) error {
|
||||||
|
sk.Key = ed25519lib.NewKeyFromSeed(seed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey generates a fresh private key with the provided randomness source.
|
||||||
|
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
|
||||||
|
publicKey, privateKey, err := ed25519lib.GenerateKey(rand)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateKeyOut := new(PrivateKey)
|
||||||
|
privateKeyOut.PublicKey.Point = publicKey[:]
|
||||||
|
privateKeyOut.Key = privateKey[:]
|
||||||
|
return privateKeyOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a message with the ed25519 algorithm.
|
||||||
|
// priv MUST be a valid key! Check this with Validate() before use.
|
||||||
|
func Sign(priv *PrivateKey, message []byte) ([]byte, error) {
|
||||||
|
return ed25519lib.Sign(priv.Key, message), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies an ed25519 signature.
|
||||||
|
func Verify(pub *PublicKey, message []byte, signature []byte) bool {
|
||||||
|
return ed25519lib.Verify(pub.Point, message, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the ed25519 private key is valid.
|
||||||
|
func Validate(priv *PrivateKey) error {
|
||||||
|
expectedPrivateKey := ed25519lib.NewKeyFromSeed(priv.Seed())
|
||||||
|
if subtle.ConstantTimeCompare(priv.Key, expectedPrivateKey) == 0 {
|
||||||
|
return errors.KeyInvalidError("ed25519: invalid ed25519 secret")
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare(priv.PublicKey.Point, expectedPrivateKey[SeedSize:]) == 0 {
|
||||||
|
return errors.KeyInvalidError("ed25519: invalid ed25519 public key")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENCODING/DECODING signature:
|
||||||
|
|
||||||
|
// WriteSignature encodes and writes an ed25519 signature to writer.
|
||||||
|
func WriteSignature(writer io.Writer, signature []byte) error {
|
||||||
|
_, err := writer.Write(signature)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSignature decodes an ed25519 signature from a reader.
|
||||||
|
func ReadSignature(reader io.Reader) ([]byte, error) {
|
||||||
|
signature := make([]byte, SignatureSize)
|
||||||
|
if _, err := io.ReadFull(reader, signature); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Package ed448 implements the ed448 signature algorithm for OpenPGP
|
||||||
|
// as defined in the Open PGP crypto refresh.
|
||||||
|
package ed448
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
ed448lib "github.com/cloudflare/circl/sign/ed448"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicKeySize is the size, in bytes, of public keys in this package.
|
||||||
|
PublicKeySize = ed448lib.PublicKeySize
|
||||||
|
// SeedSize is the size, in bytes, of private key seeds.
|
||||||
|
// The private key representation used by RFC 8032.
|
||||||
|
SeedSize = ed448lib.SeedSize
|
||||||
|
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
|
||||||
|
SignatureSize = ed448lib.SignatureSize
|
||||||
|
)
|
||||||
|
|
||||||
|
type PublicKey struct {
|
||||||
|
// Point represents the elliptic curve point of the public key.
|
||||||
|
Point []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrivateKey struct {
|
||||||
|
PublicKey
|
||||||
|
// Key the private key representation by RFC 8032,
|
||||||
|
// encoded as seed | public key point.
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey creates a new empty ed448 public key.
|
||||||
|
func NewPublicKey() *PublicKey {
|
||||||
|
return &PublicKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey creates a new empty private key referencing the public key.
|
||||||
|
func NewPrivateKey(key PublicKey) *PrivateKey {
|
||||||
|
return &PrivateKey{
|
||||||
|
PublicKey: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed returns the ed448 private key secret seed.
|
||||||
|
// The private key representation by RFC 8032.
|
||||||
|
func (pk *PrivateKey) Seed() []byte {
|
||||||
|
return pk.Key[:SeedSize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalByteSecret returns the underlying seed of the private key.
|
||||||
|
func (pk *PrivateKey) MarshalByteSecret() []byte {
|
||||||
|
return pk.Seed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalByteSecret computes the private key from the secret seed
|
||||||
|
// and stores it in the private key object.
|
||||||
|
func (sk *PrivateKey) UnmarshalByteSecret(seed []byte) error {
|
||||||
|
sk.Key = ed448lib.NewKeyFromSeed(seed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey generates a fresh private key with the provided randomness source.
|
||||||
|
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
|
||||||
|
publicKey, privateKey, err := ed448lib.GenerateKey(rand)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateKeyOut := new(PrivateKey)
|
||||||
|
privateKeyOut.PublicKey.Point = publicKey[:]
|
||||||
|
privateKeyOut.Key = privateKey[:]
|
||||||
|
return privateKeyOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a message with the ed448 algorithm.
|
||||||
|
// priv MUST be a valid key! Check this with Validate() before use.
|
||||||
|
func Sign(priv *PrivateKey, message []byte) ([]byte, error) {
|
||||||
|
// Ed448 is used with the empty string as a context string.
|
||||||
|
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7
|
||||||
|
return ed448lib.Sign(priv.Key, message, ""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies a ed448 signature
|
||||||
|
func Verify(pub *PublicKey, message []byte, signature []byte) bool {
|
||||||
|
// Ed448 is used with the empty string as a context string.
|
||||||
|
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7
|
||||||
|
return ed448lib.Verify(pub.Point, message, signature, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the ed448 private key is valid
|
||||||
|
func Validate(priv *PrivateKey) error {
|
||||||
|
expectedPrivateKey := ed448lib.NewKeyFromSeed(priv.Seed())
|
||||||
|
if subtle.ConstantTimeCompare(priv.Key, expectedPrivateKey) == 0 {
|
||||||
|
return errors.KeyInvalidError("ed448: invalid ed448 secret")
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare(priv.PublicKey.Point, expectedPrivateKey[SeedSize:]) == 0 {
|
||||||
|
return errors.KeyInvalidError("ed448: invalid ed448 public key")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENCODING/DECODING signature:
|
||||||
|
|
||||||
|
// WriteSignature encodes and writes an ed448 signature to writer.
|
||||||
|
func WriteSignature(writer io.Writer, signature []byte) error {
|
||||||
|
_, err := writer.Write(signature)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSignature decodes an ed448 signature from a reader.
|
||||||
|
func ReadSignature(reader io.Reader) ([]byte, error) {
|
||||||
|
signature := make([]byte, SignatureSize)
|
||||||
|
if _, err := io.ReadFull(reader, signature); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,18 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDecryptSessionKeyParsing is a generic error message for parsing errors in decrypted data
|
||||||
|
// to reduce the risk of oracle attacks.
|
||||||
|
ErrDecryptSessionKeyParsing = DecryptWithSessionKeyError("parsing error")
|
||||||
|
// ErrAEADTagVerification is returned if one of the tag verifications in SEIPDv2 fails
|
||||||
|
ErrAEADTagVerification error = DecryptWithSessionKeyError("AEAD tag verification failed")
|
||||||
|
// ErrMDCHashMismatch
|
||||||
|
ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
|
||||||
|
// ErrMDCMissing
|
||||||
|
ErrMDCMissing error = SignatureError("MDC packet not found")
|
||||||
|
)
|
||||||
|
|
||||||
// A StructuralError is returned when OpenPGP data is found to be syntactically
|
// A StructuralError is returned when OpenPGP data is found to be syntactically
|
||||||
// invalid.
|
// invalid.
|
||||||
type StructuralError string
|
type StructuralError string
|
||||||
|
|
@ -17,6 +29,34 @@ func (s StructuralError) Error() string {
|
||||||
return "openpgp: invalid data: " + string(s)
|
return "openpgp: invalid data: " + string(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A DecryptWithSessionKeyError is returned when a failure occurs when reading from symmetrically decrypted data or
|
||||||
|
// an authentication tag verification fails.
|
||||||
|
// Such an error indicates that the supplied session key is likely wrong or the data got corrupted.
|
||||||
|
type DecryptWithSessionKeyError string
|
||||||
|
|
||||||
|
func (s DecryptWithSessionKeyError) Error() string {
|
||||||
|
return "openpgp: decryption with session key failed: " + string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSensitiveParsingError handles parsing errors when reading data from potentially decrypted data.
|
||||||
|
// The function makes parsing errors generic to reduce the risk of oracle attacks in SEIPDv1.
|
||||||
|
func HandleSensitiveParsingError(err error, decrypted bool) error {
|
||||||
|
if !decrypted {
|
||||||
|
// Data was not encrypted so we return the inner error.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The data is read from a stream that decrypts using a session key;
|
||||||
|
// therefore, we need to handle parsing errors appropriately.
|
||||||
|
// This is essential to mitigate the risk of oracle attacks.
|
||||||
|
if decError, ok := err.(*DecryptWithSessionKeyError); ok {
|
||||||
|
return decError
|
||||||
|
}
|
||||||
|
if decError, ok := err.(DecryptWithSessionKeyError); ok {
|
||||||
|
return decError
|
||||||
|
}
|
||||||
|
return ErrDecryptSessionKeyParsing
|
||||||
|
}
|
||||||
|
|
||||||
// UnsupportedError indicates that, although the OpenPGP data is valid, it
|
// UnsupportedError indicates that, although the OpenPGP data is valid, it
|
||||||
// makes use of currently unimplemented features.
|
// makes use of currently unimplemented features.
|
||||||
type UnsupportedError string
|
type UnsupportedError string
|
||||||
|
|
@ -41,9 +81,6 @@ func (b SignatureError) Error() string {
|
||||||
return "openpgp: invalid signature: " + string(b)
|
return "openpgp: invalid signature: " + string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
|
|
||||||
var ErrMDCMissing error = SignatureError("MDC packet not found")
|
|
||||||
|
|
||||||
type signatureExpiredError int
|
type signatureExpiredError int
|
||||||
|
|
||||||
func (se signatureExpiredError) Error() string {
|
func (se signatureExpiredError) Error() string {
|
||||||
|
|
@ -58,6 +95,14 @@ func (ke keyExpiredError) Error() string {
|
||||||
return "openpgp: key expired"
|
return "openpgp: key expired"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrSignatureOlderThanKey error = signatureOlderThanKeyError(0)
|
||||||
|
|
||||||
|
type signatureOlderThanKeyError int
|
||||||
|
|
||||||
|
func (ske signatureOlderThanKeyError) Error() string {
|
||||||
|
return "openpgp: signature is older than the key"
|
||||||
|
}
|
||||||
|
|
||||||
var ErrKeyExpired error = keyExpiredError(0)
|
var ErrKeyExpired error = keyExpiredError(0)
|
||||||
|
|
||||||
type keyIncorrectError int
|
type keyIncorrectError int
|
||||||
|
|
@ -92,12 +137,24 @@ func (keyRevokedError) Error() string {
|
||||||
|
|
||||||
var ErrKeyRevoked error = keyRevokedError(0)
|
var ErrKeyRevoked error = keyRevokedError(0)
|
||||||
|
|
||||||
|
type WeakAlgorithmError string
|
||||||
|
|
||||||
|
func (e WeakAlgorithmError) Error() string {
|
||||||
|
return "openpgp: weak algorithms are rejected: " + string(e)
|
||||||
|
}
|
||||||
|
|
||||||
type UnknownPacketTypeError uint8
|
type UnknownPacketTypeError uint8
|
||||||
|
|
||||||
func (upte UnknownPacketTypeError) Error() string {
|
func (upte UnknownPacketTypeError) Error() string {
|
||||||
return "openpgp: unknown packet type: " + strconv.Itoa(int(upte))
|
return "openpgp: unknown packet type: " + strconv.Itoa(int(upte))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CriticalUnknownPacketTypeError uint8
|
||||||
|
|
||||||
|
func (upte CriticalUnknownPacketTypeError) Error() string {
|
||||||
|
return "openpgp: unknown critical packet type: " + strconv.Itoa(int(upte))
|
||||||
|
}
|
||||||
|
|
||||||
// AEADError indicates that there is a problem when initializing or using a
|
// AEADError indicates that there is a problem when initializing or using a
|
||||||
// AEAD instance, configuration struct, nonces or index values.
|
// AEAD instance, configuration struct, nonces or index values.
|
||||||
type AEADError string
|
type AEADError string
|
||||||
|
|
@ -114,3 +171,10 @@ type ErrDummyPrivateKey string
|
||||||
func (dke ErrDummyPrivateKey) Error() string {
|
func (dke ErrDummyPrivateKey) Error() string {
|
||||||
return "openpgp: s2k GNU dummy key: " + string(dke)
|
return "openpgp: s2k GNU dummy key: " + string(dke)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrMalformedMessage results when the packet sequence is incorrect
|
||||||
|
type ErrMalformedMessage string
|
||||||
|
|
||||||
|
func (dke ErrMalformedMessage) Error() string {
|
||||||
|
return "openpgp: malformed message " + string(dke)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,24 +51,14 @@ func (sk CipherFunction) Id() uint8 {
|
||||||
return uint8(sk)
|
return uint8(sk)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keySizeByID = map[uint8]int{
|
|
||||||
TripleDES.Id(): 24,
|
|
||||||
CAST5.Id(): cast5.KeySize,
|
|
||||||
AES128.Id(): 16,
|
|
||||||
AES192.Id(): 24,
|
|
||||||
AES256.Id(): 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeySize returns the key size, in bytes, of cipher.
|
// KeySize returns the key size, in bytes, of cipher.
|
||||||
func (cipher CipherFunction) KeySize() int {
|
func (cipher CipherFunction) KeySize() int {
|
||||||
switch cipher {
|
switch cipher {
|
||||||
case TripleDES:
|
|
||||||
return 24
|
|
||||||
case CAST5:
|
case CAST5:
|
||||||
return cast5.KeySize
|
return cast5.KeySize
|
||||||
case AES128:
|
case AES128:
|
||||||
return 16
|
return 16
|
||||||
case AES192:
|
case AES192, TripleDES:
|
||||||
return 24
|
return 24
|
||||||
case AES256:
|
case AES256:
|
||||||
return 32
|
return 32
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ package ecc
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/bitcurves"
|
"github.com/ProtonMail/go-crypto/bitcurves"
|
||||||
"github.com/ProtonMail/go-crypto/brainpool"
|
"github.com/ProtonMail/go-crypto/brainpool"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const Curve25519GenName = "Curve25519"
|
||||||
|
|
||||||
type CurveInfo struct {
|
type CurveInfo struct {
|
||||||
GenName string
|
GenName string
|
||||||
Oid *encoding.OID
|
Oid *encoding.OID
|
||||||
|
|
@ -42,19 +45,19 @@ var Curves = []CurveInfo{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Curve25519
|
// Curve25519
|
||||||
GenName: "Curve25519",
|
GenName: Curve25519GenName,
|
||||||
Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01}),
|
Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01}),
|
||||||
Curve: NewCurve25519(),
|
Curve: NewCurve25519(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// X448
|
// x448
|
||||||
GenName: "Curve448",
|
GenName: "Curve448",
|
||||||
Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x6F}),
|
Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x6F}),
|
||||||
Curve: NewX448(),
|
Curve: NewX448(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Ed25519
|
// Ed25519
|
||||||
GenName: "Curve25519",
|
GenName: Curve25519GenName,
|
||||||
Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01}),
|
Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01}),
|
||||||
Curve: NewEd25519(),
|
Curve: NewEd25519(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package ecc
|
package ecc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
|
@ -90,7 +91,14 @@ func (c *ed25519) GenerateEdDSA(rand io.Reader) (pub, priv []byte, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEd25519Sk(publicKey, privateKey []byte) ed25519lib.PrivateKey {
|
func getEd25519Sk(publicKey, privateKey []byte) ed25519lib.PrivateKey {
|
||||||
return append(privateKey, publicKey...)
|
privateKeyCap, privateKeyLen, publicKeyLen := cap(privateKey), len(privateKey), len(publicKey)
|
||||||
|
|
||||||
|
if privateKeyCap >= privateKeyLen+publicKeyLen &&
|
||||||
|
bytes.Equal(privateKey[privateKeyLen:privateKeyLen+publicKeyLen], publicKey) {
|
||||||
|
return privateKey[:privateKeyLen+publicKeyLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(privateKey[:privateKeyLen:privateKeyLen], publicKey...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ed25519) Sign(publicKey, privateKey, message []byte) (sig []byte, err error) {
|
func (c *ed25519) Sign(publicKey, privateKey, message []byte) (sig []byte, err error) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package ecc
|
package ecc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
|
@ -84,7 +85,14 @@ func (c *ed448) GenerateEdDSA(rand io.Reader) (pub, priv []byte, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEd448Sk(publicKey, privateKey []byte) ed448lib.PrivateKey {
|
func getEd448Sk(publicKey, privateKey []byte) ed448lib.PrivateKey {
|
||||||
return append(privateKey, publicKey...)
|
privateKeyCap, privateKeyLen, publicKeyLen := cap(privateKey), len(privateKey), len(publicKey)
|
||||||
|
|
||||||
|
if privateKeyCap >= privateKeyLen+publicKeyLen &&
|
||||||
|
bytes.Equal(privateKey[privateKeyLen:privateKeyLen+publicKeyLen], publicKey) {
|
||||||
|
return privateKey[:privateKeyLen+publicKeyLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(privateKey[:privateKeyLen:privateKeyLen], publicKey...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ed448) Sign(publicKey, privateKey, message []byte) (sig []byte, err error) {
|
func (c *ed448) Sign(publicKey, privateKey, message []byte) (sig []byte, err error) {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,15 @@ import (
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
|
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
|
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/ed25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/ed448"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
|
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x448"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewEntity returns an Entity that contains a fresh RSA/RSA keypair with a
|
// NewEntity returns an Entity that contains a fresh RSA/RSA keypair with a
|
||||||
|
|
@ -36,8 +40,10 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
primary := packet.NewSignerPrivateKey(creationTime, primaryPrivRaw)
|
primary := packet.NewSignerPrivateKey(creationTime, primaryPrivRaw)
|
||||||
if config != nil && config.V5Keys {
|
if config.V6() {
|
||||||
primary.UpgradeToV5()
|
if err := primary.UpgradeToV6(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e := &Entity{
|
e := &Entity{
|
||||||
|
|
@ -45,9 +51,25 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
|
||||||
PrivateKey: primary,
|
PrivateKey: primary,
|
||||||
Identities: make(map[string]*Identity),
|
Identities: make(map[string]*Identity),
|
||||||
Subkeys: []Subkey{},
|
Subkeys: []Subkey{},
|
||||||
|
Signatures: []*packet.Signature{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = e.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs)
|
if config.V6() {
|
||||||
|
// In v6 keys algorithm preferences should be stored in direct key signatures
|
||||||
|
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypeDirectSignature, config)
|
||||||
|
err = writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = selfSignature.SignDirectKeyBinding(&primary.PublicKey, primary, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.Signatures = append(e.Signatures, selfSignature)
|
||||||
|
e.SelfSignature = selfSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs, !config.V6())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -65,32 +87,19 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
|
||||||
func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error {
|
func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error {
|
||||||
creationTime := config.Now()
|
creationTime := config.Now()
|
||||||
keyLifetimeSecs := config.KeyLifetime()
|
keyLifetimeSecs := config.KeyLifetime()
|
||||||
return t.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs)
|
return t.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs, !config.V6())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Entity) addUserId(name, comment, email string, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32) error {
|
func writeKeyProperties(selfSignature *packet.Signature, creationTime time.Time, keyLifetimeSecs uint32, config *packet.Config) error {
|
||||||
uid := packet.NewUserId(name, comment, email)
|
advertiseAead := config.AEAD() != nil
|
||||||
if uid == nil {
|
|
||||||
return errors.InvalidArgumentError("user id field contained invalid characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := t.Identities[uid.Id]; ok {
|
|
||||||
return errors.InvalidArgumentError("user id exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
primary := t.PrivateKey
|
|
||||||
|
|
||||||
isPrimaryId := len(t.Identities) == 0
|
|
||||||
|
|
||||||
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config)
|
|
||||||
selfSignature.CreationTime = creationTime
|
selfSignature.CreationTime = creationTime
|
||||||
selfSignature.KeyLifetimeSecs = &keyLifetimeSecs
|
selfSignature.KeyLifetimeSecs = &keyLifetimeSecs
|
||||||
selfSignature.IsPrimaryId = &isPrimaryId
|
|
||||||
selfSignature.FlagsValid = true
|
selfSignature.FlagsValid = true
|
||||||
selfSignature.FlagSign = true
|
selfSignature.FlagSign = true
|
||||||
selfSignature.FlagCertify = true
|
selfSignature.FlagCertify = true
|
||||||
selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14
|
selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14
|
||||||
selfSignature.SEIPDv2 = config.AEAD() != nil
|
selfSignature.SEIPDv2 = advertiseAead
|
||||||
|
|
||||||
// Set the PreferredHash for the SelfSignature from the packet.Config.
|
// Set the PreferredHash for the SelfSignature from the packet.Config.
|
||||||
// If it is not the must-implement algorithm from rfc4880bis, append that.
|
// If it is not the must-implement algorithm from rfc4880bis, append that.
|
||||||
|
|
@ -119,18 +128,44 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c
|
||||||
selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(config.Compression()))
|
selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(config.Compression()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// And for DefaultMode.
|
if advertiseAead {
|
||||||
modes := []uint8{uint8(config.AEAD().Mode())}
|
// Get the preferred AEAD mode from the packet.Config.
|
||||||
if config.AEAD().Mode() != packet.AEADModeOCB {
|
// If it is not the must-implement algorithm from rfc9580, append that.
|
||||||
modes = append(modes, uint8(packet.AEADModeOCB))
|
modes := []uint8{uint8(config.AEAD().Mode())}
|
||||||
}
|
if config.AEAD().Mode() != packet.AEADModeOCB {
|
||||||
|
modes = append(modes, uint8(packet.AEADModeOCB))
|
||||||
|
}
|
||||||
|
|
||||||
// For preferred (AES256, GCM), we'll generate (AES256, GCM), (AES256, OCB), (AES128, GCM), (AES128, OCB)
|
// For preferred (AES256, GCM), we'll generate (AES256, GCM), (AES256, OCB), (AES128, GCM), (AES128, OCB)
|
||||||
for _, cipher := range selfSignature.PreferredSymmetric {
|
for _, cipher := range selfSignature.PreferredSymmetric {
|
||||||
for _, mode := range modes {
|
for _, mode := range modes {
|
||||||
selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode})
|
selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Entity) addUserId(name, comment, email string, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32, writeProperties bool) error {
|
||||||
|
uid := packet.NewUserId(name, comment, email)
|
||||||
|
if uid == nil {
|
||||||
|
return errors.InvalidArgumentError("user id field contained invalid characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := t.Identities[uid.Id]; ok {
|
||||||
|
return errors.InvalidArgumentError("user id exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
primary := t.PrivateKey
|
||||||
|
isPrimaryId := len(t.Identities) == 0
|
||||||
|
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config)
|
||||||
|
if writeProperties {
|
||||||
|
err := writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selfSignature.IsPrimaryId = &isPrimaryId
|
||||||
|
|
||||||
// User ID binding signature
|
// User ID binding signature
|
||||||
err := selfSignature.SignUserId(uid.Id, &primary.PublicKey, primary, config)
|
err := selfSignature.SignUserId(uid.Id, &primary.PublicKey, primary, config)
|
||||||
|
|
@ -158,8 +193,10 @@ func (e *Entity) AddSigningSubkey(config *packet.Config) error {
|
||||||
}
|
}
|
||||||
sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw)
|
sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw)
|
||||||
sub.IsSubkey = true
|
sub.IsSubkey = true
|
||||||
if config != nil && config.V5Keys {
|
if config.V6() {
|
||||||
sub.UpgradeToV5()
|
if err := sub.UpgradeToV6(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subkey := Subkey{
|
subkey := Subkey{
|
||||||
|
|
@ -203,8 +240,10 @@ func (e *Entity) addEncryptionSubkey(config *packet.Config, creationTime time.Ti
|
||||||
}
|
}
|
||||||
sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw)
|
sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw)
|
||||||
sub.IsSubkey = true
|
sub.IsSubkey = true
|
||||||
if config != nil && config.V5Keys {
|
if config.V6() {
|
||||||
sub.UpgradeToV5()
|
if err := sub.UpgradeToV6(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subkey := Subkey{
|
subkey := Subkey{
|
||||||
|
|
@ -242,6 +281,11 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
|
||||||
}
|
}
|
||||||
return rsa.GenerateKey(config.Random(), bits)
|
return rsa.GenerateKey(config.Random(), bits)
|
||||||
case packet.PubKeyAlgoEdDSA:
|
case packet.PubKeyAlgoEdDSA:
|
||||||
|
if config.V6() {
|
||||||
|
// Implementations MUST NOT accept or generate v6 key material
|
||||||
|
// using the deprecated OIDs.
|
||||||
|
return nil, errors.InvalidArgumentError("EdDSALegacy cannot be used for v6 keys")
|
||||||
|
}
|
||||||
curve := ecc.FindEdDSAByGenName(string(config.CurveName()))
|
curve := ecc.FindEdDSAByGenName(string(config.CurveName()))
|
||||||
if curve == nil {
|
if curve == nil {
|
||||||
return nil, errors.InvalidArgumentError("unsupported curve")
|
return nil, errors.InvalidArgumentError("unsupported curve")
|
||||||
|
|
@ -263,6 +307,18 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return priv, nil
|
return priv, nil
|
||||||
|
case packet.PubKeyAlgoEd25519:
|
||||||
|
priv, err := ed25519.GenerateKey(config.Random())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return priv, nil
|
||||||
|
case packet.PubKeyAlgoEd448:
|
||||||
|
priv, err := ed448.GenerateKey(config.Random())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return priv, nil
|
||||||
default:
|
default:
|
||||||
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
|
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
|
||||||
}
|
}
|
||||||
|
|
@ -285,6 +341,13 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
|
||||||
case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA:
|
case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA:
|
||||||
fallthrough // When passing EdDSA or ECDSA, we generate an ECDH subkey
|
fallthrough // When passing EdDSA or ECDSA, we generate an ECDH subkey
|
||||||
case packet.PubKeyAlgoECDH:
|
case packet.PubKeyAlgoECDH:
|
||||||
|
if config.V6() &&
|
||||||
|
(config.CurveName() == packet.Curve25519 ||
|
||||||
|
config.CurveName() == packet.Curve448) {
|
||||||
|
// Implementations MUST NOT accept or generate v6 key material
|
||||||
|
// using the deprecated OIDs.
|
||||||
|
return nil, errors.InvalidArgumentError("ECDH with Curve25519/448 legacy cannot be used for v6 keys")
|
||||||
|
}
|
||||||
var kdf = ecdh.KDF{
|
var kdf = ecdh.KDF{
|
||||||
Hash: algorithm.SHA512,
|
Hash: algorithm.SHA512,
|
||||||
Cipher: algorithm.AES256,
|
Cipher: algorithm.AES256,
|
||||||
|
|
@ -294,6 +357,10 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
|
||||||
return nil, errors.InvalidArgumentError("unsupported curve")
|
return nil, errors.InvalidArgumentError("unsupported curve")
|
||||||
}
|
}
|
||||||
return ecdh.GenerateKey(config.Random(), curve, kdf)
|
return ecdh.GenerateKey(config.Random(), curve, kdf)
|
||||||
|
case packet.PubKeyAlgoEd25519, packet.PubKeyAlgoX25519: // When passing Ed25519, we generate an x25519 subkey
|
||||||
|
return x25519.GenerateKey(config.Random())
|
||||||
|
case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey
|
||||||
|
return x448.GenerateKey(config.Random())
|
||||||
default:
|
default:
|
||||||
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
|
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +369,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
|
||||||
var bigOne = big.NewInt(1)
|
var bigOne = big.NewInt(1)
|
||||||
|
|
||||||
// generateRSAKeyWithPrimes generates a multi-prime RSA keypair of the
|
// generateRSAKeyWithPrimes generates a multi-prime RSA keypair of the
|
||||||
// given bit size, using the given random source and prepopulated primes.
|
// given bit size, using the given random source and pre-populated primes.
|
||||||
func generateRSAKeyWithPrimes(random io.Reader, nprimes int, bits int, prepopulatedPrimes []*big.Int) (*rsa.PrivateKey, error) {
|
func generateRSAKeyWithPrimes(random io.Reader, nprimes int, bits int, prepopulatedPrimes []*big.Int) (*rsa.PrivateKey, error) {
|
||||||
priv := new(rsa.PrivateKey)
|
priv := new(rsa.PrivateKey)
|
||||||
priv.E = 65537
|
priv.E = 65537
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package openpgp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
goerrors "errors"
|
goerrors "errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -24,11 +25,13 @@ var PrivateKeyType = "PGP PRIVATE KEY BLOCK"
|
||||||
// (which must be a signing key), one or more identities claimed by that key,
|
// (which must be a signing key), one or more identities claimed by that key,
|
||||||
// and zero or more subkeys, which may be encryption keys.
|
// and zero or more subkeys, which may be encryption keys.
|
||||||
type Entity struct {
|
type Entity struct {
|
||||||
PrimaryKey *packet.PublicKey
|
PrimaryKey *packet.PublicKey
|
||||||
PrivateKey *packet.PrivateKey
|
PrivateKey *packet.PrivateKey
|
||||||
Identities map[string]*Identity // indexed by Identity.Name
|
Identities map[string]*Identity // indexed by Identity.Name
|
||||||
Revocations []*packet.Signature
|
Revocations []*packet.Signature
|
||||||
Subkeys []Subkey
|
Subkeys []Subkey
|
||||||
|
SelfSignature *packet.Signature // Direct-key self signature of the PrimaryKey (contains primary key properties in v6)
|
||||||
|
Signatures []*packet.Signature // all (potentially unverified) self-signatures, revocations, and third-party signatures
|
||||||
}
|
}
|
||||||
|
|
||||||
// An Identity represents an identity claimed by an Entity and zero or more
|
// An Identity represents an identity claimed by an Entity and zero or more
|
||||||
|
|
@ -120,12 +123,12 @@ func shouldPreferIdentity(existingId, potentialNewId *Identity) bool {
|
||||||
// given Entity.
|
// given Entity.
|
||||||
func (e *Entity) EncryptionKey(now time.Time) (Key, bool) {
|
func (e *Entity) EncryptionKey(now time.Time) (Key, bool) {
|
||||||
// Fail to find any encryption key if the...
|
// Fail to find any encryption key if the...
|
||||||
i := e.PrimaryIdentity()
|
primarySelfSignature, primaryIdentity := e.PrimarySelfSignature()
|
||||||
if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired
|
if primarySelfSignature == nil || // no self-signature found
|
||||||
i.SelfSignature == nil || // user ID has no self-signature
|
e.PrimaryKey.KeyExpired(primarySelfSignature, now) || // primary key has expired
|
||||||
i.SelfSignature.SigExpired(now) || // user ID self-signature has expired
|
|
||||||
e.Revoked(now) || // primary key has been revoked
|
e.Revoked(now) || // primary key has been revoked
|
||||||
i.Revoked(now) { // user ID has been revoked
|
primarySelfSignature.SigExpired(now) || // user ID or or direct self-signature has expired
|
||||||
|
(primaryIdentity != nil && primaryIdentity.Revoked(now)) { // user ID has been revoked (for v4 keys)
|
||||||
return Key{}, false
|
return Key{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,9 +155,9 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) {
|
||||||
|
|
||||||
// If we don't have any subkeys for encryption and the primary key
|
// If we don't have any subkeys for encryption and the primary key
|
||||||
// is marked as OK to encrypt with, then we can use it.
|
// is marked as OK to encrypt with, then we can use it.
|
||||||
if i.SelfSignature.FlagsValid && i.SelfSignature.FlagEncryptCommunications &&
|
if primarySelfSignature.FlagsValid && primarySelfSignature.FlagEncryptCommunications &&
|
||||||
e.PrimaryKey.PubKeyAlgo.CanEncrypt() {
|
e.PrimaryKey.PubKeyAlgo.CanEncrypt() {
|
||||||
return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true
|
return Key{e, e.PrimaryKey, e.PrivateKey, primarySelfSignature, e.Revocations}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return Key{}, false
|
return Key{}, false
|
||||||
|
|
@ -186,12 +189,12 @@ func (e *Entity) SigningKeyById(now time.Time, id uint64) (Key, bool) {
|
||||||
|
|
||||||
func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, bool) {
|
func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, bool) {
|
||||||
// Fail to find any signing key if the...
|
// Fail to find any signing key if the...
|
||||||
i := e.PrimaryIdentity()
|
primarySelfSignature, primaryIdentity := e.PrimarySelfSignature()
|
||||||
if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired
|
if primarySelfSignature == nil || // no self-signature found
|
||||||
i.SelfSignature == nil || // user ID has no self-signature
|
e.PrimaryKey.KeyExpired(primarySelfSignature, now) || // primary key has expired
|
||||||
i.SelfSignature.SigExpired(now) || // user ID self-signature has expired
|
|
||||||
e.Revoked(now) || // primary key has been revoked
|
e.Revoked(now) || // primary key has been revoked
|
||||||
i.Revoked(now) { // user ID has been revoked
|
primarySelfSignature.SigExpired(now) || // user ID or direct self-signature has expired
|
||||||
|
(primaryIdentity != nil && primaryIdentity.Revoked(now)) { // user ID has been revoked (for v4 keys)
|
||||||
return Key{}, false
|
return Key{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,12 +223,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key,
|
||||||
|
|
||||||
// If we don't have any subkeys for signing and the primary key
|
// If we don't have any subkeys for signing and the primary key
|
||||||
// is marked as OK to sign with, then we can use it.
|
// is marked as OK to sign with, then we can use it.
|
||||||
if i.SelfSignature.FlagsValid &&
|
if primarySelfSignature.FlagsValid &&
|
||||||
(flags&packet.KeyFlagCertify == 0 || i.SelfSignature.FlagCertify) &&
|
(flags&packet.KeyFlagCertify == 0 || primarySelfSignature.FlagCertify) &&
|
||||||
(flags&packet.KeyFlagSign == 0 || i.SelfSignature.FlagSign) &&
|
(flags&packet.KeyFlagSign == 0 || primarySelfSignature.FlagSign) &&
|
||||||
e.PrimaryKey.PubKeyAlgo.CanSign() &&
|
e.PrimaryKey.PubKeyAlgo.CanSign() &&
|
||||||
(id == 0 || e.PrimaryKey.KeyId == id) {
|
(id == 0 || e.PrimaryKey.KeyId == id) {
|
||||||
return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true
|
return Key{e, e.PrimaryKey, e.PrivateKey, primarySelfSignature, e.Revocations}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// No keys with a valid Signing Flag or no keys matched the id passed in
|
// No keys with a valid Signing Flag or no keys matched the id passed in
|
||||||
|
|
@ -259,7 +262,7 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er
|
||||||
var keysToEncrypt []*packet.PrivateKey
|
var keysToEncrypt []*packet.PrivateKey
|
||||||
// Add entity private key to encrypt.
|
// Add entity private key to encrypt.
|
||||||
if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted {
|
if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted {
|
||||||
keysToEncrypt = append(keysToEncrypt, e.PrivateKey)
|
keysToEncrypt = append(keysToEncrypt, e.PrivateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add subkeys to encrypt.
|
// Add subkeys to encrypt.
|
||||||
|
|
@ -271,7 +274,7 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er
|
||||||
return packet.EncryptPrivateKeys(keysToEncrypt, passphrase, config)
|
return packet.EncryptPrivateKeys(keysToEncrypt, passphrase, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptPrivateKeys decrypts all encrypted keys in the entitiy with the given passphrase.
|
// DecryptPrivateKeys decrypts all encrypted keys in the entity with the given passphrase.
|
||||||
// Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored,
|
// Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored,
|
||||||
// and don't cause an error to be returned.
|
// and don't cause an error to be returned.
|
||||||
func (e *Entity) DecryptPrivateKeys(passphrase []byte) error {
|
func (e *Entity) DecryptPrivateKeys(passphrase []byte) error {
|
||||||
|
|
@ -284,7 +287,7 @@ func (e *Entity) DecryptPrivateKeys(passphrase []byte) error {
|
||||||
// Add subkeys to decrypt.
|
// Add subkeys to decrypt.
|
||||||
for _, sub := range e.Subkeys {
|
for _, sub := range e.Subkeys {
|
||||||
if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && sub.PrivateKey.Encrypted {
|
if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && sub.PrivateKey.Encrypted {
|
||||||
keysToDecrypt = append(keysToDecrypt, sub.PrivateKey)
|
keysToDecrypt = append(keysToDecrypt, sub.PrivateKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return packet.DecryptPrivateKeys(keysToDecrypt, passphrase)
|
return packet.DecryptPrivateKeys(keysToDecrypt, passphrase)
|
||||||
|
|
@ -318,8 +321,7 @@ type EntityList []*Entity
|
||||||
func (el EntityList) KeysById(id uint64) (keys []Key) {
|
func (el EntityList) KeysById(id uint64) (keys []Key) {
|
||||||
for _, e := range el {
|
for _, e := range el {
|
||||||
if e.PrimaryKey.KeyId == id {
|
if e.PrimaryKey.KeyId == id {
|
||||||
ident := e.PrimaryIdentity()
|
selfSig, _ := e.PrimarySelfSignature()
|
||||||
selfSig := ident.SelfSignature
|
|
||||||
keys = append(keys, Key{e, e.PrimaryKey, e.PrivateKey, selfSig, e.Revocations})
|
keys = append(keys, Key{e, e.PrimaryKey, e.PrivateKey, selfSig, e.Revocations})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -441,7 +443,6 @@ func readToNextPublicKey(packets *packet.Reader) (err error) {
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
if _, ok := err.(errors.UnsupportedError); ok {
|
if _, ok := err.(errors.UnsupportedError); ok {
|
||||||
err = nil
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
@ -479,6 +480,7 @@ func ReadEntity(packets *packet.Reader) (*Entity, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var revocations []*packet.Signature
|
var revocations []*packet.Signature
|
||||||
|
var directSignatures []*packet.Signature
|
||||||
EachPacket:
|
EachPacket:
|
||||||
for {
|
for {
|
||||||
p, err := packets.Next()
|
p, err := packets.Next()
|
||||||
|
|
@ -497,9 +499,7 @@ EachPacket:
|
||||||
if pkt.SigType == packet.SigTypeKeyRevocation {
|
if pkt.SigType == packet.SigTypeKeyRevocation {
|
||||||
revocations = append(revocations, pkt)
|
revocations = append(revocations, pkt)
|
||||||
} else if pkt.SigType == packet.SigTypeDirectSignature {
|
} else if pkt.SigType == packet.SigTypeDirectSignature {
|
||||||
// TODO: RFC4880 5.2.1 permits signatures
|
directSignatures = append(directSignatures, pkt)
|
||||||
// directly on keys (eg. to bind additional
|
|
||||||
// revocation keys).
|
|
||||||
}
|
}
|
||||||
// Else, ignoring the signature as it does not follow anything
|
// Else, ignoring the signature as it does not follow anything
|
||||||
// we would know to attach it to.
|
// we would know to attach it to.
|
||||||
|
|
@ -522,12 +522,39 @@ EachPacket:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// we ignore unknown packets
|
// we ignore unknown packets.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(e.Identities) == 0 {
|
if len(e.Identities) == 0 && e.PrimaryKey.Version < 6 {
|
||||||
return nil, errors.StructuralError("entity without any identities")
|
return nil, errors.StructuralError(fmt.Sprintf("v%d entity without any identities", e.PrimaryKey.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
// An implementation MUST ensure that a valid direct-key signature is present before using a v6 key.
|
||||||
|
if e.PrimaryKey.Version == 6 {
|
||||||
|
if len(directSignatures) == 0 {
|
||||||
|
return nil, errors.StructuralError("v6 entity without a valid direct-key signature")
|
||||||
|
}
|
||||||
|
// Select main direct key signature.
|
||||||
|
var mainDirectKeySelfSignature *packet.Signature
|
||||||
|
for _, directSignature := range directSignatures {
|
||||||
|
if directSignature.SigType == packet.SigTypeDirectSignature &&
|
||||||
|
directSignature.CheckKeyIdOrFingerprint(e.PrimaryKey) &&
|
||||||
|
(mainDirectKeySelfSignature == nil ||
|
||||||
|
directSignature.CreationTime.After(mainDirectKeySelfSignature.CreationTime)) {
|
||||||
|
mainDirectKeySelfSignature = directSignature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mainDirectKeySelfSignature == nil {
|
||||||
|
return nil, errors.StructuralError("no valid direct-key self-signature for v6 primary key found")
|
||||||
|
}
|
||||||
|
// Check that the main self-signature is valid.
|
||||||
|
err = e.PrimaryKey.VerifyDirectKeySignature(mainDirectKeySelfSignature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.StructuralError("invalid direct-key self-signature for v6 primary key")
|
||||||
|
}
|
||||||
|
e.SelfSignature = mainDirectKeySelfSignature
|
||||||
|
e.Signatures = directSignatures
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, revocation := range revocations {
|
for _, revocation := range revocations {
|
||||||
|
|
@ -672,6 +699,12 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, directSignature := range e.Signatures {
|
||||||
|
err := directSignature.Serialize(w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, ident := range e.Identities {
|
for _, ident := range e.Identities {
|
||||||
err = ident.UserId.Serialize(w)
|
err = ident.UserId.Serialize(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -738,6 +771,12 @@ func (e *Entity) Serialize(w io.Writer) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, directSignature := range e.Signatures {
|
||||||
|
err := directSignature.Serialize(w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, ident := range e.Identities {
|
for _, ident := range e.Identities {
|
||||||
err = ident.UserId.Serialize(w)
|
err = ident.UserId.Serialize(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -840,3 +879,23 @@ func (e *Entity) RevokeSubkey(sk *Subkey, reason packet.ReasonForRevocation, rea
|
||||||
sk.Revocations = append(sk.Revocations, revSig)
|
sk.Revocations = append(sk.Revocations, revSig)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Entity) primaryDirectSignature() *packet.Signature {
|
||||||
|
return e.SelfSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrimarySelfSignature searches the entity for the self-signature that stores key preferences.
|
||||||
|
// For V4 keys, returns the self-signature of the primary identity, and the identity.
|
||||||
|
// For V6 keys, returns the latest valid direct-key self-signature, and no identity (nil).
|
||||||
|
// This self-signature is to be used to check the key expiration,
|
||||||
|
// algorithm preferences, and so on.
|
||||||
|
func (e *Entity) PrimarySelfSignature() (*packet.Signature, *Identity) {
|
||||||
|
if e.PrimaryKey.Version == 6 {
|
||||||
|
return e.primaryDirectSignature(), nil
|
||||||
|
}
|
||||||
|
primaryIdentity := e.PrimaryIdentity()
|
||||||
|
if primaryIdentity == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return primaryIdentity.SelfSignature, primaryIdentity
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,17 +88,20 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
|
||||||
if errRead != nil && errRead != io.EOF {
|
if errRead != nil && errRead != io.EOF {
|
||||||
return 0, errRead
|
return 0, errRead
|
||||||
}
|
}
|
||||||
decrypted, errChunk := ar.openChunk(cipherChunk)
|
|
||||||
if errChunk != nil {
|
|
||||||
return 0, errChunk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return decrypted bytes, buffering if necessary
|
if len(cipherChunk) > 0 {
|
||||||
if len(dst) < len(decrypted) {
|
decrypted, errChunk := ar.openChunk(cipherChunk)
|
||||||
n = copy(dst, decrypted[:len(dst)])
|
if errChunk != nil {
|
||||||
ar.buffer.Write(decrypted[len(dst):])
|
return 0, errChunk
|
||||||
} else {
|
}
|
||||||
n = copy(dst, decrypted)
|
|
||||||
|
// Return decrypted bytes, buffering if necessary
|
||||||
|
if len(dst) < len(decrypted) {
|
||||||
|
n = copy(dst, decrypted[:len(dst)])
|
||||||
|
ar.buffer.Write(decrypted[len(dst):])
|
||||||
|
} else {
|
||||||
|
n = copy(dst, decrypted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check final authentication tag
|
// Check final authentication tag
|
||||||
|
|
@ -116,6 +119,12 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
|
||||||
// checked in the last Read call. In the future, this function could be used to
|
// checked in the last Read call. In the future, this function could be used to
|
||||||
// wipe the reader and peeked, decrypted bytes, if necessary.
|
// wipe the reader and peeked, decrypted bytes, if necessary.
|
||||||
func (ar *aeadDecrypter) Close() (err error) {
|
func (ar *aeadDecrypter) Close() (err error) {
|
||||||
|
if !ar.eof {
|
||||||
|
errChunk := ar.validateFinalTag(ar.peekedBytes)
|
||||||
|
if errChunk != nil {
|
||||||
|
return errChunk
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +147,7 @@ func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
|
||||||
nonce := ar.computeNextNonce()
|
nonce := ar.computeNextNonce()
|
||||||
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
|
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.ErrAEADTagVerification
|
||||||
}
|
}
|
||||||
ar.bytesProcessed += len(plainChunk)
|
ar.bytesProcessed += len(plainChunk)
|
||||||
if err = ar.aeadCrypter.incrementIndex(); err != nil {
|
if err = ar.aeadCrypter.incrementIndex(); err != nil {
|
||||||
|
|
@ -163,9 +172,8 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
|
||||||
// ... and total number of encrypted octets
|
// ... and total number of encrypted octets
|
||||||
adata = append(adata, amountBytes...)
|
adata = append(adata, amountBytes...)
|
||||||
nonce := ar.computeNextNonce()
|
nonce := ar.computeNextNonce()
|
||||||
_, err := ar.aead.Open(nil, nonce, tag, adata)
|
if _, err := ar.aead.Open(nil, nonce, tag, adata); err != nil {
|
||||||
if err != nil {
|
return errors.ErrAEADTagVerification
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ import (
|
||||||
"compress/bzip2"
|
"compress/bzip2"
|
||||||
"compress/flate"
|
"compress/flate"
|
||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compressed represents a compressed OpenPGP packet. The decompressed contents
|
// Compressed represents a compressed OpenPGP packet. The decompressed contents
|
||||||
|
|
@ -39,6 +40,37 @@ type CompressionConfig struct {
|
||||||
Level int
|
Level int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decompressionReader ensures that the whole compression packet is read.
|
||||||
|
type decompressionReader struct {
|
||||||
|
compressed io.Reader
|
||||||
|
decompressed io.ReadCloser
|
||||||
|
readAll bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecompressionReader(r io.Reader, decompressor io.ReadCloser) *decompressionReader {
|
||||||
|
return &decompressionReader{
|
||||||
|
compressed: r,
|
||||||
|
decompressed: decompressor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *decompressionReader) Read(data []byte) (n int, err error) {
|
||||||
|
if dr.readAll {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n, err = dr.decompressed.Read(data)
|
||||||
|
if err == io.EOF {
|
||||||
|
dr.readAll = true
|
||||||
|
// Close the decompressor.
|
||||||
|
if errDec := dr.decompressed.Close(); errDec != nil {
|
||||||
|
return n, errDec
|
||||||
|
}
|
||||||
|
// Consume all remaining data from the compressed packet.
|
||||||
|
consumeAll(dr.compressed)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Compressed) parse(r io.Reader) error {
|
func (c *Compressed) parse(r io.Reader) error {
|
||||||
var buf [1]byte
|
var buf [1]byte
|
||||||
_, err := readFull(r, buf[:])
|
_, err := readFull(r, buf[:])
|
||||||
|
|
@ -50,11 +82,15 @@ func (c *Compressed) parse(r io.Reader) error {
|
||||||
case 0:
|
case 0:
|
||||||
c.Body = r
|
c.Body = r
|
||||||
case 1:
|
case 1:
|
||||||
c.Body = flate.NewReader(r)
|
c.Body = newDecompressionReader(r, flate.NewReader(r))
|
||||||
case 2:
|
case 2:
|
||||||
c.Body, err = zlib.NewReader(r)
|
decompressor, err := zlib.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Body = newDecompressionReader(r, decompressor)
|
||||||
case 3:
|
case 3:
|
||||||
c.Body = bzip2.NewReader(r)
|
c.Body = newDecompressionReader(r, io.NopCloser(bzip2.NewReader(r)))
|
||||||
default:
|
default:
|
||||||
err = errors.UnsupportedError("unknown compression algorithm: " + strconv.Itoa(int(buf[0])))
|
err = errors.UnsupportedError("unknown compression algorithm: " + strconv.Itoa(int(buf[0])))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,34 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/s2k"
|
"github.com/ProtonMail/go-crypto/openpgp/s2k"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultRejectPublicKeyAlgorithms = map[PublicKeyAlgorithm]bool{
|
||||||
|
PubKeyAlgoElGamal: true,
|
||||||
|
PubKeyAlgoDSA: true,
|
||||||
|
}
|
||||||
|
defaultRejectHashAlgorithms = map[crypto.Hash]bool{
|
||||||
|
crypto.MD5: true,
|
||||||
|
crypto.RIPEMD160: true,
|
||||||
|
}
|
||||||
|
defaultRejectMessageHashAlgorithms = map[crypto.Hash]bool{
|
||||||
|
crypto.SHA1: true,
|
||||||
|
crypto.MD5: true,
|
||||||
|
crypto.RIPEMD160: true,
|
||||||
|
}
|
||||||
|
defaultRejectCurves = map[Curve]bool{
|
||||||
|
CurveSecP256k1: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// A global feature flag to indicate v5 support.
|
||||||
|
// Can be set via a build tag, e.g.: `go build -tags v5 ./...`
|
||||||
|
// If the build tag is missing config_v5.go will set it to true.
|
||||||
|
//
|
||||||
|
// Disables parsing of v5 keys and v5 signatures.
|
||||||
|
// These are non-standard entities, which in the crypto-refresh have been superseded
|
||||||
|
// by v6 keys, v6 signatures and SEIPDv2 encrypted data, respectively.
|
||||||
|
var V5Disabled = false
|
||||||
|
|
||||||
// Config collects a number of parameters along with sensible defaults.
|
// Config collects a number of parameters along with sensible defaults.
|
||||||
// A nil *Config is valid and results in all default values.
|
// A nil *Config is valid and results in all default values.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -73,9 +101,16 @@ type Config struct {
|
||||||
// **Note: using this option may break compatibility with other OpenPGP
|
// **Note: using this option may break compatibility with other OpenPGP
|
||||||
// implementations, as well as future versions of this library.**
|
// implementations, as well as future versions of this library.**
|
||||||
AEADConfig *AEADConfig
|
AEADConfig *AEADConfig
|
||||||
// V5Keys configures version 5 key generation. If false, this package still
|
// V6Keys configures version 6 key generation. If false, this package still
|
||||||
// supports version 5 keys, but produces version 4 keys.
|
// supports version 6 keys, but produces version 4 keys.
|
||||||
V5Keys bool
|
V6Keys bool
|
||||||
|
// Minimum RSA key size allowed for key generation and message signing, verification and encryption.
|
||||||
|
MinRSABits uint16
|
||||||
|
// Reject insecure algorithms, only works with v2 api
|
||||||
|
RejectPublicKeyAlgorithms map[PublicKeyAlgorithm]bool
|
||||||
|
RejectHashAlgorithms map[crypto.Hash]bool
|
||||||
|
RejectMessageHashAlgorithms map[crypto.Hash]bool
|
||||||
|
RejectCurves map[Curve]bool
|
||||||
// "The validity period of the key. This is the number of seconds after
|
// "The validity period of the key. This is the number of seconds after
|
||||||
// the key creation time that the key expires. If this is not present
|
// the key creation time that the key expires. If this is not present
|
||||||
// or has a value of zero, the key never expires. This is found only on
|
// or has a value of zero, the key never expires. This is found only on
|
||||||
|
|
@ -104,12 +139,40 @@ type Config struct {
|
||||||
// might be no other way than to tolerate the missing MDC. Setting this flag, allows this
|
// might be no other way than to tolerate the missing MDC. Setting this flag, allows this
|
||||||
// mode of operation. It should be considered a measure of last resort.
|
// mode of operation. It should be considered a measure of last resort.
|
||||||
InsecureAllowUnauthenticatedMessages bool
|
InsecureAllowUnauthenticatedMessages bool
|
||||||
|
// InsecureAllowDecryptionWithSigningKeys allows decryption with keys marked as signing keys in the v2 API.
|
||||||
|
// This setting is potentially insecure, but it is needed as some libraries
|
||||||
|
// ignored key flags when selecting a key for encryption.
|
||||||
|
// Not relevant for the v1 API, as all keys were allowed in decryption.
|
||||||
|
InsecureAllowDecryptionWithSigningKeys bool
|
||||||
// KnownNotations is a map of Notation Data names to bools, which controls
|
// KnownNotations is a map of Notation Data names to bools, which controls
|
||||||
// the notation names that are allowed to be present in critical Notation Data
|
// the notation names that are allowed to be present in critical Notation Data
|
||||||
// signature subpackets.
|
// signature subpackets.
|
||||||
KnownNotations map[string]bool
|
KnownNotations map[string]bool
|
||||||
// SignatureNotations is a list of Notations to be added to any signatures.
|
// SignatureNotations is a list of Notations to be added to any signatures.
|
||||||
SignatureNotations []*Notation
|
SignatureNotations []*Notation
|
||||||
|
// CheckIntendedRecipients controls, whether the OpenPGP Intended Recipient Fingerprint feature
|
||||||
|
// should be enabled for encryption and decryption.
|
||||||
|
// (See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#name-intended-recipient-fingerpr).
|
||||||
|
// When the flag is set, encryption produces Intended Recipient Fingerprint signature sub-packets and decryption
|
||||||
|
// checks whether the key it was encrypted to is one of the included fingerprints in the signature.
|
||||||
|
// If the flag is disabled, no Intended Recipient Fingerprint sub-packets are created or checked.
|
||||||
|
// The default behavior, when the config or flag is nil, is to enable the feature.
|
||||||
|
CheckIntendedRecipients *bool
|
||||||
|
// CacheSessionKey controls if decryption should return the session key used for decryption.
|
||||||
|
// If the flag is set, the session key is cached in the message details struct.
|
||||||
|
CacheSessionKey bool
|
||||||
|
// CheckPacketSequence is a flag that controls if the pgp message reader should strictly check
|
||||||
|
// that the packet sequence conforms with the grammar mandated by rfc4880.
|
||||||
|
// The default behavior, when the config or flag is nil, is to check the packet sequence.
|
||||||
|
CheckPacketSequence *bool
|
||||||
|
// NonDeterministicSignaturesViaNotation is a flag to enable randomization of signatures.
|
||||||
|
// If true, a salt notation is used to randomize signatures generated by v4 and v5 keys
|
||||||
|
// (v6 signatures are always non-deterministic, by design).
|
||||||
|
// This protects EdDSA signatures from potentially leaking the secret key in case of faults (i.e. bitflips) which, in principle, could occur
|
||||||
|
// during the signing computation. It is added to signatures of any algo for simplicity, and as it may also serve as protection in case of
|
||||||
|
// weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks.
|
||||||
|
// The default behavior, when the config or flag is nil, is to enable the feature.
|
||||||
|
NonDeterministicSignaturesViaNotation *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Random() io.Reader {
|
func (c *Config) Random() io.Reader {
|
||||||
|
|
@ -197,7 +260,7 @@ func (c *Config) S2K() *s2k.Config {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// for backwards compatibility
|
// for backwards compatibility
|
||||||
if c != nil && c.S2KCount > 0 && c.S2KConfig == nil {
|
if c.S2KCount > 0 && c.S2KConfig == nil {
|
||||||
return &s2k.Config{
|
return &s2k.Config{
|
||||||
S2KCount: c.S2KCount,
|
S2KCount: c.S2KCount,
|
||||||
}
|
}
|
||||||
|
|
@ -233,6 +296,13 @@ func (c *Config) AllowUnauthenticatedMessages() bool {
|
||||||
return c.InsecureAllowUnauthenticatedMessages
|
return c.InsecureAllowUnauthenticatedMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) AllowDecryptionWithSigningKeys() bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.InsecureAllowDecryptionWithSigningKeys
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) KnownNotation(notationName string) bool {
|
func (c *Config) KnownNotation(notationName string) bool {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return false
|
return false
|
||||||
|
|
@ -246,3 +316,95 @@ func (c *Config) Notations() []*Notation {
|
||||||
}
|
}
|
||||||
return c.SignatureNotations
|
return c.SignatureNotations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) V6() bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.V6Keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IntendedRecipients() bool {
|
||||||
|
if c == nil || c.CheckIntendedRecipients == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *c.CheckIntendedRecipients
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RetrieveSessionKey() bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.CacheSessionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) MinimumRSABits() uint16 {
|
||||||
|
if c == nil || c.MinRSABits == 0 {
|
||||||
|
return 2047
|
||||||
|
}
|
||||||
|
return c.MinRSABits
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RejectPublicKeyAlgorithm(alg PublicKeyAlgorithm) bool {
|
||||||
|
var rejectedAlgorithms map[PublicKeyAlgorithm]bool
|
||||||
|
if c == nil || c.RejectPublicKeyAlgorithms == nil {
|
||||||
|
// Default
|
||||||
|
rejectedAlgorithms = defaultRejectPublicKeyAlgorithms
|
||||||
|
} else {
|
||||||
|
rejectedAlgorithms = c.RejectPublicKeyAlgorithms
|
||||||
|
}
|
||||||
|
return rejectedAlgorithms[alg]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RejectHashAlgorithm(hash crypto.Hash) bool {
|
||||||
|
var rejectedAlgorithms map[crypto.Hash]bool
|
||||||
|
if c == nil || c.RejectHashAlgorithms == nil {
|
||||||
|
// Default
|
||||||
|
rejectedAlgorithms = defaultRejectHashAlgorithms
|
||||||
|
} else {
|
||||||
|
rejectedAlgorithms = c.RejectHashAlgorithms
|
||||||
|
}
|
||||||
|
return rejectedAlgorithms[hash]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RejectMessageHashAlgorithm(hash crypto.Hash) bool {
|
||||||
|
var rejectedAlgorithms map[crypto.Hash]bool
|
||||||
|
if c == nil || c.RejectMessageHashAlgorithms == nil {
|
||||||
|
// Default
|
||||||
|
rejectedAlgorithms = defaultRejectMessageHashAlgorithms
|
||||||
|
} else {
|
||||||
|
rejectedAlgorithms = c.RejectMessageHashAlgorithms
|
||||||
|
}
|
||||||
|
return rejectedAlgorithms[hash]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RejectCurve(curve Curve) bool {
|
||||||
|
var rejectedCurve map[Curve]bool
|
||||||
|
if c == nil || c.RejectCurves == nil {
|
||||||
|
// Default
|
||||||
|
rejectedCurve = defaultRejectCurves
|
||||||
|
} else {
|
||||||
|
rejectedCurve = c.RejectCurves
|
||||||
|
}
|
||||||
|
return rejectedCurve[curve]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) StrictPacketSequence() bool {
|
||||||
|
if c == nil || c.CheckPacketSequence == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *c.CheckPacketSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RandomizeSignaturesViaNotation() bool {
|
||||||
|
if c == nil || c.NonDeterministicSignaturesViaNotation == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *c.NonDeterministicSignaturesViaNotation
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolPointer is a helper function to set a boolean pointer in the Config.
|
||||||
|
// e.g., config.CheckPacketSequence = BoolPointer(true)
|
||||||
|
func BoolPointer(value bool) *bool {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
//go:build !v5
|
||||||
|
|
||||||
|
package packet
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
V5Disabled = true
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -16,32 +18,85 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x448"
|
||||||
)
|
)
|
||||||
|
|
||||||
const encryptedKeyVersion = 3
|
|
||||||
|
|
||||||
// EncryptedKey represents a public-key encrypted session key. See RFC 4880,
|
// EncryptedKey represents a public-key encrypted session key. See RFC 4880,
|
||||||
// section 5.1.
|
// section 5.1.
|
||||||
type EncryptedKey struct {
|
type EncryptedKey struct {
|
||||||
KeyId uint64
|
Version int
|
||||||
Algo PublicKeyAlgorithm
|
KeyId uint64
|
||||||
CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet
|
KeyVersion int // v6
|
||||||
Key []byte // only valid after a successful Decrypt
|
KeyFingerprint []byte // v6
|
||||||
|
Algo PublicKeyAlgorithm
|
||||||
|
CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet
|
||||||
|
Key []byte // only valid after a successful Decrypt
|
||||||
|
|
||||||
encryptedMPI1, encryptedMPI2 encoding.Field
|
encryptedMPI1, encryptedMPI2 encoding.Field
|
||||||
|
ephemeralPublicX25519 *x25519.PublicKey // used for x25519
|
||||||
|
ephemeralPublicX448 *x448.PublicKey // used for x448
|
||||||
|
encryptedSession []byte // used for x25519 and x448
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EncryptedKey) parse(r io.Reader) (err error) {
|
func (e *EncryptedKey) parse(r io.Reader) (err error) {
|
||||||
var buf [10]byte
|
var buf [8]byte
|
||||||
_, err = readFull(r, buf[:])
|
_, err = readFull(r, buf[:versionSize])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if buf[0] != encryptedKeyVersion {
|
e.Version = int(buf[0])
|
||||||
|
if e.Version != 3 && e.Version != 6 {
|
||||||
return errors.UnsupportedError("unknown EncryptedKey version " + strconv.Itoa(int(buf[0])))
|
return errors.UnsupportedError("unknown EncryptedKey version " + strconv.Itoa(int(buf[0])))
|
||||||
}
|
}
|
||||||
e.KeyId = binary.BigEndian.Uint64(buf[1:9])
|
if e.Version == 6 {
|
||||||
e.Algo = PublicKeyAlgorithm(buf[9])
|
//Read a one-octet size of the following two fields.
|
||||||
|
if _, err = readFull(r, buf[:1]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The size may also be zero, and the key version and
|
||||||
|
// fingerprint omitted for an "anonymous recipient"
|
||||||
|
if buf[0] != 0 {
|
||||||
|
// non-anonymous case
|
||||||
|
_, err = readFull(r, buf[:versionSize])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.KeyVersion = int(buf[0])
|
||||||
|
if e.KeyVersion != 4 && e.KeyVersion != 6 {
|
||||||
|
return errors.UnsupportedError("unknown public key version " + strconv.Itoa(e.KeyVersion))
|
||||||
|
}
|
||||||
|
var fingerprint []byte
|
||||||
|
if e.KeyVersion == 6 {
|
||||||
|
fingerprint = make([]byte, fingerprintSizeV6)
|
||||||
|
} else if e.KeyVersion == 4 {
|
||||||
|
fingerprint = make([]byte, fingerprintSize)
|
||||||
|
}
|
||||||
|
_, err = readFull(r, fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.KeyFingerprint = fingerprint
|
||||||
|
if e.KeyVersion == 6 {
|
||||||
|
e.KeyId = binary.BigEndian.Uint64(e.KeyFingerprint[:keyIdSize])
|
||||||
|
} else if e.KeyVersion == 4 {
|
||||||
|
e.KeyId = binary.BigEndian.Uint64(e.KeyFingerprint[fingerprintSize-keyIdSize : fingerprintSize])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = readFull(r, buf[:8])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.KeyId = binary.BigEndian.Uint64(buf[:keyIdSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = readFull(r, buf[:1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Algo = PublicKeyAlgorithm(buf[0])
|
||||||
|
var cipherFunction byte
|
||||||
switch e.Algo {
|
switch e.Algo {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
||||||
e.encryptedMPI1 = new(encoding.MPI)
|
e.encryptedMPI1 = new(encoding.MPI)
|
||||||
|
|
@ -68,26 +123,39 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) {
|
||||||
if _, err = e.encryptedMPI2.ReadFrom(r); err != nil {
|
if _, err = e.encryptedMPI2.ReadFrom(r); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
e.ephemeralPublicX25519, e.encryptedSession, cipherFunction, err = x25519.DecodeFields(r, e.Version == 6)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
e.ephemeralPublicX448, e.encryptedSession, cipherFunction, err = x448.DecodeFields(r, e.Version == 6)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if e.Version < 6 {
|
||||||
|
switch e.Algo {
|
||||||
|
case PubKeyAlgoX25519, PubKeyAlgoX448:
|
||||||
|
e.CipherFunc = CipherFunction(cipherFunction)
|
||||||
|
// Check for validiy is in the Decrypt method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err = consumeAll(r)
|
_, err = consumeAll(r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func checksumKeyMaterial(key []byte) uint16 {
|
|
||||||
var checksum uint16
|
|
||||||
for _, v := range key {
|
|
||||||
checksum += uint16(v)
|
|
||||||
}
|
|
||||||
return checksum
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt decrypts an encrypted session key with the given private key. The
|
// Decrypt decrypts an encrypted session key with the given private key. The
|
||||||
// private key must have been decrypted first.
|
// private key must have been decrypted first.
|
||||||
// If config is nil, sensible defaults will be used.
|
// If config is nil, sensible defaults will be used.
|
||||||
func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error {
|
func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error {
|
||||||
if e.KeyId != 0 && e.KeyId != priv.KeyId {
|
if e.Version < 6 && e.KeyId != 0 && e.KeyId != priv.KeyId {
|
||||||
return errors.InvalidArgumentError("cannot decrypt encrypted session key for key id " + strconv.FormatUint(e.KeyId, 16) + " with private key id " + strconv.FormatUint(priv.KeyId, 16))
|
return errors.InvalidArgumentError("cannot decrypt encrypted session key for key id " + strconv.FormatUint(e.KeyId, 16) + " with private key id " + strconv.FormatUint(priv.KeyId, 16))
|
||||||
}
|
}
|
||||||
|
if e.Version == 6 && e.KeyVersion != 0 && !bytes.Equal(e.KeyFingerprint, priv.Fingerprint) {
|
||||||
|
return errors.InvalidArgumentError("cannot decrypt encrypted session key for key fingerprint " + hex.EncodeToString(e.KeyFingerprint) + " with private key fingerprint " + hex.EncodeToString(priv.Fingerprint))
|
||||||
|
}
|
||||||
if e.Algo != priv.PubKeyAlgo {
|
if e.Algo != priv.PubKeyAlgo {
|
||||||
return errors.InvalidArgumentError("cannot decrypt encrypted session key of type " + strconv.Itoa(int(e.Algo)) + " with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo)))
|
return errors.InvalidArgumentError("cannot decrypt encrypted session key of type " + strconv.Itoa(int(e.Algo)) + " with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo)))
|
||||||
}
|
}
|
||||||
|
|
@ -113,52 +181,116 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error {
|
||||||
vsG := e.encryptedMPI1.Bytes()
|
vsG := e.encryptedMPI1.Bytes()
|
||||||
m := e.encryptedMPI2.Bytes()
|
m := e.encryptedMPI2.Bytes()
|
||||||
oid := priv.PublicKey.oid.EncodedBytes()
|
oid := priv.PublicKey.oid.EncodedBytes()
|
||||||
b, err = ecdh.Decrypt(priv.PrivateKey.(*ecdh.PrivateKey), vsG, m, oid, priv.PublicKey.Fingerprint[:])
|
fp := priv.PublicKey.Fingerprint[:]
|
||||||
|
if priv.PublicKey.Version == 5 {
|
||||||
|
// For v5 the, the fingerprint must be restricted to 20 bytes
|
||||||
|
fp = fp[:20]
|
||||||
|
}
|
||||||
|
b, err = ecdh.Decrypt(priv.PrivateKey.(*ecdh.PrivateKey), vsG, m, oid, fp)
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
b, err = x25519.Decrypt(priv.PrivateKey.(*x25519.PrivateKey), e.ephemeralPublicX25519, e.encryptedSession)
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession)
|
||||||
default:
|
default:
|
||||||
err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo)))
|
err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.CipherFunc = CipherFunction(b[0])
|
var key []byte
|
||||||
if !e.CipherFunc.IsSupported() {
|
switch priv.PubKeyAlgo {
|
||||||
return errors.UnsupportedError("unsupported encryption function")
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
|
||||||
|
keyOffset := 0
|
||||||
|
if e.Version < 6 {
|
||||||
|
e.CipherFunc = CipherFunction(b[0])
|
||||||
|
keyOffset = 1
|
||||||
|
if !e.CipherFunc.IsSupported() {
|
||||||
|
return errors.UnsupportedError("unsupported encryption function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key, err = decodeChecksumKey(b[keyOffset:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case PubKeyAlgoX25519, PubKeyAlgoX448:
|
||||||
|
if e.Version < 6 {
|
||||||
|
switch e.CipherFunc {
|
||||||
|
case CipherAES128, CipherAES192, CipherAES256:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519 and x448")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key = b[:]
|
||||||
|
default:
|
||||||
|
return errors.UnsupportedError("unsupported algorithm for decryption")
|
||||||
}
|
}
|
||||||
|
e.Key = key
|
||||||
e.Key = b[1 : len(b)-2]
|
|
||||||
expectedChecksum := uint16(b[len(b)-2])<<8 | uint16(b[len(b)-1])
|
|
||||||
checksum := checksumKeyMaterial(e.Key)
|
|
||||||
if checksum != expectedChecksum {
|
|
||||||
return errors.StructuralError("EncryptedKey checksum incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize writes the encrypted key packet, e, to w.
|
// Serialize writes the encrypted key packet, e, to w.
|
||||||
func (e *EncryptedKey) Serialize(w io.Writer) error {
|
func (e *EncryptedKey) Serialize(w io.Writer) error {
|
||||||
var mpiLen int
|
var encodedLength int
|
||||||
switch e.Algo {
|
switch e.Algo {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
||||||
mpiLen = int(e.encryptedMPI1.EncodedLength())
|
encodedLength = int(e.encryptedMPI1.EncodedLength())
|
||||||
case PubKeyAlgoElGamal:
|
case PubKeyAlgoElGamal:
|
||||||
mpiLen = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
|
encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
|
||||||
case PubKeyAlgoECDH:
|
case PubKeyAlgoECDH:
|
||||||
mpiLen = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
|
encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
encodedLength = x25519.EncodedFieldsLength(e.encryptedSession, e.Version == 6)
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
encodedLength = x448.EncodedFieldsLength(e.encryptedSession, e.Version == 6)
|
||||||
default:
|
default:
|
||||||
return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo)))
|
return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo)))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := serializeHeader(w, packetTypeEncryptedKey, 1 /* version */ +8 /* key id */ +1 /* algo */ +mpiLen)
|
packetLen := versionSize /* version */ + keyIdSize /* key id */ + algorithmSize /* algo */ + encodedLength
|
||||||
|
if e.Version == 6 {
|
||||||
|
packetLen = versionSize /* version */ + algorithmSize /* algo */ + encodedLength + keyVersionSize /* key version */
|
||||||
|
if e.KeyVersion == 6 {
|
||||||
|
packetLen += fingerprintSizeV6
|
||||||
|
} else if e.KeyVersion == 4 {
|
||||||
|
packetLen += fingerprintSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write([]byte{encryptedKeyVersion})
|
_, err = w.Write([]byte{byte(e.Version)})
|
||||||
binary.Write(w, binary.BigEndian, e.KeyId)
|
if err != nil {
|
||||||
w.Write([]byte{byte(e.Algo)})
|
return err
|
||||||
|
}
|
||||||
|
if e.Version == 6 {
|
||||||
|
_, err = w.Write([]byte{byte(e.KeyVersion)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The key version number may also be zero,
|
||||||
|
// and the fingerprint omitted
|
||||||
|
if e.KeyVersion != 0 {
|
||||||
|
_, err = w.Write(e.KeyFingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Write KeyID
|
||||||
|
err = binary.Write(w, binary.BigEndian, e.KeyId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte{byte(e.Algo)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
switch e.Algo {
|
switch e.Algo {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
||||||
|
|
@ -176,34 +308,115 @@ func (e *EncryptedKey) Serialize(w io.Writer) error {
|
||||||
}
|
}
|
||||||
_, err := w.Write(e.encryptedMPI2.EncodedBytes())
|
_, err := w.Write(e.encryptedMPI2.EncodedBytes())
|
||||||
return err
|
return err
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
err := x25519.EncodeFields(w, e.ephemeralPublicX25519, e.encryptedSession, byte(e.CipherFunc), e.Version == 6)
|
||||||
|
return err
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
err := x448.EncodeFields(w, e.ephemeralPublicX448, e.encryptedSession, byte(e.CipherFunc), e.Version == 6)
|
||||||
|
return err
|
||||||
default:
|
default:
|
||||||
panic("internal error")
|
panic("internal error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerializeEncryptedKey serializes an encrypted key packet to w that contains
|
// SerializeEncryptedKeyAEAD serializes an encrypted key packet to w that contains
|
||||||
// key, encrypted to pub.
|
// key, encrypted to pub.
|
||||||
|
// If aeadSupported is set, PKESK v6 is used, otherwise v3.
|
||||||
|
// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted.
|
||||||
// If config is nil, sensible defaults will be used.
|
// If config is nil, sensible defaults will be used.
|
||||||
func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, config *Config) error {
|
func SerializeEncryptedKeyAEAD(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, config *Config) error {
|
||||||
var buf [10]byte
|
return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, aeadSupported, key, false, config)
|
||||||
buf[0] = encryptedKeyVersion
|
}
|
||||||
binary.BigEndian.PutUint64(buf[1:9], pub.KeyId)
|
|
||||||
buf[9] = byte(pub.PubKeyAlgo)
|
|
||||||
|
|
||||||
keyBlock := make([]byte, 1 /* cipher type */ +len(key)+2 /* checksum */)
|
// SerializeEncryptedKeyAEADwithHiddenOption serializes an encrypted key packet to w that contains
|
||||||
keyBlock[0] = byte(cipherFunc)
|
// key, encrypted to pub.
|
||||||
copy(keyBlock[1:], key)
|
// Offers the hidden flag option to indicated if the PKESK packet should include a wildcard KeyID.
|
||||||
checksum := checksumKeyMaterial(key)
|
// If aeadSupported is set, PKESK v6 is used, otherwise v3.
|
||||||
keyBlock[1+len(key)] = byte(checksum >> 8)
|
// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted.
|
||||||
keyBlock[1+len(key)+1] = byte(checksum)
|
// If config is nil, sensible defaults will be used.
|
||||||
|
func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, hidden bool, config *Config) error {
|
||||||
|
var buf [36]byte // max possible header size is v6
|
||||||
|
lenHeaderWritten := versionSize
|
||||||
|
version := 3
|
||||||
|
|
||||||
|
if aeadSupported {
|
||||||
|
version = 6
|
||||||
|
}
|
||||||
|
// An implementation MUST NOT generate ElGamal v6 PKESKs.
|
||||||
|
if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal {
|
||||||
|
return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed")
|
||||||
|
}
|
||||||
|
// In v3 PKESKs, for x25519 and x448, mandate using AES
|
||||||
|
if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) {
|
||||||
|
switch cipherFunc {
|
||||||
|
case CipherAES128, CipherAES192, CipherAES256:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf[0] = byte(version)
|
||||||
|
|
||||||
|
// If hidden is set, the key should be hidden
|
||||||
|
// An implementation MAY accept or use a Key ID of all zeros,
|
||||||
|
// or a key version of zero and no key fingerprint, to hide the intended decryption key.
|
||||||
|
// See Section 5.1.8. in the open pgp crypto refresh
|
||||||
|
if version == 6 {
|
||||||
|
if !hidden {
|
||||||
|
// A one-octet size of the following two fields.
|
||||||
|
buf[1] = byte(keyVersionSize + len(pub.Fingerprint))
|
||||||
|
// A one octet key version number.
|
||||||
|
buf[2] = byte(pub.Version)
|
||||||
|
lenHeaderWritten += keyVersionSize + 1
|
||||||
|
// The fingerprint of the public key
|
||||||
|
copy(buf[lenHeaderWritten:lenHeaderWritten+len(pub.Fingerprint)], pub.Fingerprint)
|
||||||
|
lenHeaderWritten += len(pub.Fingerprint)
|
||||||
|
} else {
|
||||||
|
// The size may also be zero, and the key version
|
||||||
|
// and fingerprint omitted for an "anonymous recipient"
|
||||||
|
buf[1] = 0
|
||||||
|
lenHeaderWritten += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !hidden {
|
||||||
|
binary.BigEndian.PutUint64(buf[versionSize:(versionSize+keyIdSize)], pub.KeyId)
|
||||||
|
}
|
||||||
|
lenHeaderWritten += keyIdSize
|
||||||
|
}
|
||||||
|
buf[lenHeaderWritten] = byte(pub.PubKeyAlgo)
|
||||||
|
lenHeaderWritten += algorithmSize
|
||||||
|
|
||||||
|
var keyBlock []byte
|
||||||
|
switch pub.PubKeyAlgo {
|
||||||
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
|
||||||
|
lenKeyBlock := len(key) + 2
|
||||||
|
if version < 6 {
|
||||||
|
lenKeyBlock += 1 // cipher type included
|
||||||
|
}
|
||||||
|
keyBlock = make([]byte, lenKeyBlock)
|
||||||
|
keyOffset := 0
|
||||||
|
if version < 6 {
|
||||||
|
keyBlock[0] = byte(cipherFunc)
|
||||||
|
keyOffset = 1
|
||||||
|
}
|
||||||
|
encodeChecksumKey(keyBlock[keyOffset:], key)
|
||||||
|
case PubKeyAlgoX25519, PubKeyAlgoX448:
|
||||||
|
// algorithm is added in plaintext below
|
||||||
|
keyBlock = key
|
||||||
|
}
|
||||||
|
|
||||||
switch pub.PubKeyAlgo {
|
switch pub.PubKeyAlgo {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
|
||||||
return serializeEncryptedKeyRSA(w, config.Random(), buf, pub.PublicKey.(*rsa.PublicKey), keyBlock)
|
return serializeEncryptedKeyRSA(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*rsa.PublicKey), keyBlock)
|
||||||
case PubKeyAlgoElGamal:
|
case PubKeyAlgoElGamal:
|
||||||
return serializeEncryptedKeyElGamal(w, config.Random(), buf, pub.PublicKey.(*elgamal.PublicKey), keyBlock)
|
return serializeEncryptedKeyElGamal(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*elgamal.PublicKey), keyBlock)
|
||||||
case PubKeyAlgoECDH:
|
case PubKeyAlgoECDH:
|
||||||
return serializeEncryptedKeyECDH(w, config.Random(), buf, pub.PublicKey.(*ecdh.PublicKey), keyBlock, pub.oid, pub.Fingerprint)
|
return serializeEncryptedKeyECDH(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*ecdh.PublicKey), keyBlock, pub.oid, pub.Fingerprint)
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
return serializeEncryptedKeyX25519(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x25519.PublicKey), keyBlock, byte(cipherFunc), version)
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version)
|
||||||
case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly:
|
case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly:
|
||||||
return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo)))
|
return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo)))
|
||||||
}
|
}
|
||||||
|
|
@ -211,14 +424,32 @@ func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunctio
|
||||||
return errors.UnsupportedError("encrypting a key to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo)))
|
return errors.UnsupportedError("encrypting a key to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header [10]byte, pub *rsa.PublicKey, keyBlock []byte) error {
|
// SerializeEncryptedKey serializes an encrypted key packet to w that contains
|
||||||
|
// key, encrypted to pub.
|
||||||
|
// PKESKv6 is used if config.AEAD() is not nil.
|
||||||
|
// If config is nil, sensible defaults will be used.
|
||||||
|
// Deprecated: Use SerializeEncryptedKeyAEAD instead.
|
||||||
|
func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, config *Config) error {
|
||||||
|
return SerializeEncryptedKeyAEAD(w, pub, cipherFunc, config.AEAD() != nil, key, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeEncryptedKeyWithHiddenOption serializes an encrypted key packet to w that contains
|
||||||
|
// key, encrypted to pub. PKESKv6 is used if config.AEAD() is not nil.
|
||||||
|
// The hidden option controls if the packet should be anonymous, i.e., omit key metadata.
|
||||||
|
// If config is nil, sensible defaults will be used.
|
||||||
|
// Deprecated: Use SerializeEncryptedKeyAEADwithHiddenOption instead.
|
||||||
|
func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, hidden bool, config *Config) error {
|
||||||
|
return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error {
|
||||||
cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock)
|
cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InvalidArgumentError("RSA encryption failed: " + err.Error())
|
return errors.InvalidArgumentError("RSA encryption failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
cipherMPI := encoding.NewMPI(cipherText)
|
cipherMPI := encoding.NewMPI(cipherText)
|
||||||
packetLen := 10 /* header length */ + int(cipherMPI.EncodedLength())
|
packetLen := len(header) /* header length */ + int(cipherMPI.EncodedLength())
|
||||||
|
|
||||||
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -232,13 +463,13 @@ func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header [10]byte, pub
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header [10]byte, pub *elgamal.PublicKey, keyBlock []byte) error {
|
func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header []byte, pub *elgamal.PublicKey, keyBlock []byte) error {
|
||||||
c1, c2, err := elgamal.Encrypt(rand, pub, keyBlock)
|
c1, c2, err := elgamal.Encrypt(rand, pub, keyBlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InvalidArgumentError("ElGamal encryption failed: " + err.Error())
|
return errors.InvalidArgumentError("ElGamal encryption failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
packetLen := 10 /* header length */
|
packetLen := len(header) /* header length */
|
||||||
packetLen += 2 /* mpi size */ + (c1.BitLen()+7)/8
|
packetLen += 2 /* mpi size */ + (c1.BitLen()+7)/8
|
||||||
packetLen += 2 /* mpi size */ + (c2.BitLen()+7)/8
|
packetLen += 2 /* mpi size */ + (c2.BitLen()+7)/8
|
||||||
|
|
||||||
|
|
@ -257,7 +488,7 @@ func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header [10]byte,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub *ecdh.PublicKey, keyBlock []byte, oid encoding.Field, fingerprint []byte) error {
|
func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header []byte, pub *ecdh.PublicKey, keyBlock []byte, oid encoding.Field, fingerprint []byte) error {
|
||||||
vsG, c, err := ecdh.Encrypt(rand, pub, keyBlock, oid.EncodedBytes(), fingerprint)
|
vsG, c, err := ecdh.Encrypt(rand, pub, keyBlock, oid.EncodedBytes(), fingerprint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InvalidArgumentError("ECDH encryption failed: " + err.Error())
|
return errors.InvalidArgumentError("ECDH encryption failed: " + err.Error())
|
||||||
|
|
@ -266,7 +497,7 @@ func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub
|
||||||
g := encoding.NewMPI(vsG)
|
g := encoding.NewMPI(vsG)
|
||||||
m := encoding.NewOID(c)
|
m := encoding.NewOID(c)
|
||||||
|
|
||||||
packetLen := 10 /* header length */
|
packetLen := len(header) /* header length */
|
||||||
packetLen += int(g.EncodedLength()) + int(m.EncodedLength())
|
packetLen += int(g.EncodedLength()) + int(m.EncodedLength())
|
||||||
|
|
||||||
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
||||||
|
|
@ -284,3 +515,70 @@ func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub
|
||||||
_, err = w.Write(m.EncodedBytes())
|
_, err = w.Write(m.EncodedBytes())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serializeEncryptedKeyX25519(w io.Writer, rand io.Reader, header []byte, pub *x25519.PublicKey, keyBlock []byte, cipherFunc byte, version int) error {
|
||||||
|
ephemeralPublicX25519, ciphertext, err := x25519.Encrypt(rand, pub, keyBlock)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidArgumentError("x25519 encryption failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
packetLen := len(header) /* header length */
|
||||||
|
packetLen += x25519.EncodedFieldsLength(ciphertext, version == 6)
|
||||||
|
|
||||||
|
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(header[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return x25519.EncodeFields(w, ephemeralPublicX25519, ciphertext, cipherFunc, version == 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub *x448.PublicKey, keyBlock []byte, cipherFunc byte, version int) error {
|
||||||
|
ephemeralPublicX448, ciphertext, err := x448.Encrypt(rand, pub, keyBlock)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidArgumentError("x448 encryption failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
packetLen := len(header) /* header length */
|
||||||
|
packetLen += x448.EncodedFieldsLength(ciphertext, version == 6)
|
||||||
|
|
||||||
|
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(header[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checksumKeyMaterial(key []byte) uint16 {
|
||||||
|
var checksum uint16
|
||||||
|
for _, v := range key {
|
||||||
|
checksum += uint16(v)
|
||||||
|
}
|
||||||
|
return checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeChecksumKey(msg []byte) (key []byte, err error) {
|
||||||
|
key = msg[:len(msg)-2]
|
||||||
|
expectedChecksum := uint16(msg[len(msg)-2])<<8 | uint16(msg[len(msg)-1])
|
||||||
|
checksum := checksumKeyMaterial(key)
|
||||||
|
if checksum != expectedChecksum {
|
||||||
|
err = errors.StructuralError("session key checksum is incorrect")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeChecksumKey(buffer []byte, key []byte) {
|
||||||
|
copy(buffer, key)
|
||||||
|
checksum := checksumKeyMaterial(key)
|
||||||
|
buffer[len(key)] = byte(checksum >> 8)
|
||||||
|
buffer[len(key)+1] = byte(checksum)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@ func (l *LiteralData) parse(r io.Reader) (err error) {
|
||||||
// on completion. The fileName is truncated to 255 bytes.
|
// on completion. The fileName is truncated to 255 bytes.
|
||||||
func SerializeLiteral(w io.WriteCloser, isBinary bool, fileName string, time uint32) (plaintext io.WriteCloser, err error) {
|
func SerializeLiteral(w io.WriteCloser, isBinary bool, fileName string, time uint32) (plaintext io.WriteCloser, err error) {
|
||||||
var buf [4]byte
|
var buf [4]byte
|
||||||
buf[0] = 't'
|
buf[0] = 'b'
|
||||||
if isBinary {
|
if !isBinary {
|
||||||
buf[0] = 'b'
|
buf[0] = 'u'
|
||||||
}
|
}
|
||||||
if len(fileName) > 255 {
|
if len(fileName) > 255 {
|
||||||
fileName = fileName[:255]
|
fileName = fileName[:255]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package packet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Marker struct{}
|
||||||
|
|
||||||
|
const markerString = "PGP"
|
||||||
|
|
||||||
|
// parse just checks if the packet contains "PGP".
|
||||||
|
func (m *Marker) parse(reader io.Reader) error {
|
||||||
|
var buffer [3]byte
|
||||||
|
if _, err := io.ReadFull(reader, buffer[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if string(buffer[:]) != markerString {
|
||||||
|
return errors.StructuralError("invalid marker packet")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeMarker writes a marker packet to writer.
|
||||||
|
func SerializeMarker(writer io.Writer) error {
|
||||||
|
err := serializeHeader(writer, packetTypeMarker, len(markerString))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = writer.Write([]byte(markerString))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -7,34 +7,37 @@ package packet
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
|
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnePassSignature represents a one-pass signature packet. See RFC 4880,
|
// OnePassSignature represents a one-pass signature packet. See RFC 4880,
|
||||||
// section 5.4.
|
// section 5.4.
|
||||||
type OnePassSignature struct {
|
type OnePassSignature struct {
|
||||||
SigType SignatureType
|
Version int
|
||||||
Hash crypto.Hash
|
SigType SignatureType
|
||||||
PubKeyAlgo PublicKeyAlgorithm
|
Hash crypto.Hash
|
||||||
KeyId uint64
|
PubKeyAlgo PublicKeyAlgorithm
|
||||||
IsLast bool
|
KeyId uint64
|
||||||
|
IsLast bool
|
||||||
|
Salt []byte // v6 only
|
||||||
|
KeyFingerprint []byte // v6 only
|
||||||
}
|
}
|
||||||
|
|
||||||
const onePassSignatureVersion = 3
|
|
||||||
|
|
||||||
func (ops *OnePassSignature) parse(r io.Reader) (err error) {
|
func (ops *OnePassSignature) parse(r io.Reader) (err error) {
|
||||||
var buf [13]byte
|
var buf [8]byte
|
||||||
|
// Read: version | signature type | hash algorithm | public-key algorithm
|
||||||
_, err = readFull(r, buf[:])
|
_, err = readFull(r, buf[:4])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if buf[0] != onePassSignatureVersion {
|
if buf[0] != 3 && buf[0] != 6 {
|
||||||
err = errors.UnsupportedError("one-pass-signature packet version " + strconv.Itoa(int(buf[0])))
|
return errors.UnsupportedError("one-pass-signature packet version " + strconv.Itoa(int(buf[0])))
|
||||||
}
|
}
|
||||||
|
ops.Version = int(buf[0])
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
ops.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2])
|
ops.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2])
|
||||||
|
|
@ -44,15 +47,69 @@ func (ops *OnePassSignature) parse(r io.Reader) (err error) {
|
||||||
|
|
||||||
ops.SigType = SignatureType(buf[1])
|
ops.SigType = SignatureType(buf[1])
|
||||||
ops.PubKeyAlgo = PublicKeyAlgorithm(buf[3])
|
ops.PubKeyAlgo = PublicKeyAlgorithm(buf[3])
|
||||||
ops.KeyId = binary.BigEndian.Uint64(buf[4:12])
|
|
||||||
ops.IsLast = buf[12] != 0
|
if ops.Version == 6 {
|
||||||
|
// Only for v6, a variable-length field containing the salt
|
||||||
|
_, err = readFull(r, buf[:1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saltLength := int(buf[0])
|
||||||
|
var expectedSaltLength int
|
||||||
|
expectedSaltLength, err = SaltLengthForHash(ops.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if saltLength != expectedSaltLength {
|
||||||
|
err = errors.StructuralError("unexpected salt size for the given hash algorithm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
salt := make([]byte, expectedSaltLength)
|
||||||
|
_, err = readFull(r, salt)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ops.Salt = salt
|
||||||
|
|
||||||
|
// Only for v6 packets, 32 octets of the fingerprint of the signing key.
|
||||||
|
fingerprint := make([]byte, 32)
|
||||||
|
_, err = readFull(r, fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ops.KeyFingerprint = fingerprint
|
||||||
|
ops.KeyId = binary.BigEndian.Uint64(ops.KeyFingerprint[:8])
|
||||||
|
} else {
|
||||||
|
_, err = readFull(r, buf[:8])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ops.KeyId = binary.BigEndian.Uint64(buf[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = readFull(r, buf[:1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ops.IsLast = buf[0] != 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize marshals the given OnePassSignature to w.
|
// Serialize marshals the given OnePassSignature to w.
|
||||||
func (ops *OnePassSignature) Serialize(w io.Writer) error {
|
func (ops *OnePassSignature) Serialize(w io.Writer) error {
|
||||||
var buf [13]byte
|
//v3 length 1+1+1+1+8+1 =
|
||||||
buf[0] = onePassSignatureVersion
|
packetLength := 13
|
||||||
|
if ops.Version == 6 {
|
||||||
|
// v6 length 1+1+1+1+1+len(salt)+32+1 =
|
||||||
|
packetLength = 38 + len(ops.Salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serializeHeader(w, packetTypeOnePassSignature, packetLength); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [8]byte
|
||||||
|
buf[0] = byte(ops.Version)
|
||||||
buf[1] = uint8(ops.SigType)
|
buf[1] = uint8(ops.SigType)
|
||||||
var ok bool
|
var ok bool
|
||||||
buf[2], ok = algorithm.HashToHashIdWithSha1(ops.Hash)
|
buf[2], ok = algorithm.HashToHashIdWithSha1(ops.Hash)
|
||||||
|
|
@ -60,14 +117,41 @@ func (ops *OnePassSignature) Serialize(w io.Writer) error {
|
||||||
return errors.UnsupportedError("hash type: " + strconv.Itoa(int(ops.Hash)))
|
return errors.UnsupportedError("hash type: " + strconv.Itoa(int(ops.Hash)))
|
||||||
}
|
}
|
||||||
buf[3] = uint8(ops.PubKeyAlgo)
|
buf[3] = uint8(ops.PubKeyAlgo)
|
||||||
binary.BigEndian.PutUint64(buf[4:12], ops.KeyId)
|
|
||||||
if ops.IsLast {
|
|
||||||
buf[12] = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := serializeHeader(w, packetTypeOnePassSignature, len(buf)); err != nil {
|
_, err := w.Write(buf[:4])
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := w.Write(buf[:])
|
|
||||||
|
if ops.Version == 6 {
|
||||||
|
// write salt for v6 signatures
|
||||||
|
_, err := w.Write([]byte{uint8(len(ops.Salt))})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(ops.Salt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write fingerprint v6 signatures
|
||||||
|
_, err = w.Write(ops.KeyFingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binary.BigEndian.PutUint64(buf[:8], ops.KeyId)
|
||||||
|
_, err := w.Write(buf[:8])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLast := []byte{byte(0)}
|
||||||
|
if ops.IsLast {
|
||||||
|
isLast[0] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(isLast)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ package packet
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
)
|
)
|
||||||
|
|
@ -26,7 +25,7 @@ type OpaquePacket struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (op *OpaquePacket) parse(r io.Reader) (err error) {
|
func (op *OpaquePacket) parse(r io.Reader) (err error) {
|
||||||
op.Contents, err = ioutil.ReadAll(r)
|
op.Contents, err = io.ReadAll(r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -311,12 +311,15 @@ const (
|
||||||
packetTypePrivateSubkey packetType = 7
|
packetTypePrivateSubkey packetType = 7
|
||||||
packetTypeCompressed packetType = 8
|
packetTypeCompressed packetType = 8
|
||||||
packetTypeSymmetricallyEncrypted packetType = 9
|
packetTypeSymmetricallyEncrypted packetType = 9
|
||||||
|
packetTypeMarker packetType = 10
|
||||||
packetTypeLiteralData packetType = 11
|
packetTypeLiteralData packetType = 11
|
||||||
|
packetTypeTrust packetType = 12
|
||||||
packetTypeUserId packetType = 13
|
packetTypeUserId packetType = 13
|
||||||
packetTypePublicSubkey packetType = 14
|
packetTypePublicSubkey packetType = 14
|
||||||
packetTypeUserAttribute packetType = 17
|
packetTypeUserAttribute packetType = 17
|
||||||
packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18
|
packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18
|
||||||
packetTypeAEADEncrypted packetType = 20
|
packetTypeAEADEncrypted packetType = 20
|
||||||
|
packetPadding packetType = 21
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncryptedDataPacket holds encrypted data. It is currently implemented by
|
// EncryptedDataPacket holds encrypted data. It is currently implemented by
|
||||||
|
|
@ -328,7 +331,7 @@ type EncryptedDataPacket interface {
|
||||||
// Read reads a single OpenPGP packet from the given io.Reader. If there is an
|
// Read reads a single OpenPGP packet from the given io.Reader. If there is an
|
||||||
// error parsing a packet, the whole packet is consumed from the input.
|
// error parsing a packet, the whole packet is consumed from the input.
|
||||||
func Read(r io.Reader) (p Packet, err error) {
|
func Read(r io.Reader) (p Packet, err error) {
|
||||||
tag, _, contents, err := readHeader(r)
|
tag, len, contents, err := readHeader(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -367,8 +370,93 @@ func Read(r io.Reader) (p Packet, err error) {
|
||||||
p = se
|
p = se
|
||||||
case packetTypeAEADEncrypted:
|
case packetTypeAEADEncrypted:
|
||||||
p = new(AEADEncrypted)
|
p = new(AEADEncrypted)
|
||||||
default:
|
case packetPadding:
|
||||||
|
p = Padding(len)
|
||||||
|
case packetTypeMarker:
|
||||||
|
p = new(Marker)
|
||||||
|
case packetTypeTrust:
|
||||||
|
// Not implemented, just consume
|
||||||
err = errors.UnknownPacketTypeError(tag)
|
err = errors.UnknownPacketTypeError(tag)
|
||||||
|
default:
|
||||||
|
// Packet Tags from 0 to 39 are critical.
|
||||||
|
// Packet Tags from 40 to 63 are non-critical.
|
||||||
|
if tag < 40 {
|
||||||
|
err = errors.CriticalUnknownPacketTypeError(tag)
|
||||||
|
} else {
|
||||||
|
err = errors.UnknownPacketTypeError(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p != nil {
|
||||||
|
err = p.parse(contents)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
consumeAll(contents)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadWithCheck reads a single OpenPGP message packet from the given io.Reader. If there is an
|
||||||
|
// error parsing a packet, the whole packet is consumed from the input.
|
||||||
|
// ReadWithCheck additionally checks if the OpenPGP message packet sequence adheres
|
||||||
|
// to the packet composition rules in rfc4880, if not throws an error.
|
||||||
|
func ReadWithCheck(r io.Reader, sequence *SequenceVerifier) (p Packet, msgErr error, err error) {
|
||||||
|
tag, len, contents, err := readHeader(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch tag {
|
||||||
|
case packetTypeEncryptedKey:
|
||||||
|
msgErr = sequence.Next(ESKSymbol)
|
||||||
|
p = new(EncryptedKey)
|
||||||
|
case packetTypeSignature:
|
||||||
|
msgErr = sequence.Next(SigSymbol)
|
||||||
|
p = new(Signature)
|
||||||
|
case packetTypeSymmetricKeyEncrypted:
|
||||||
|
msgErr = sequence.Next(ESKSymbol)
|
||||||
|
p = new(SymmetricKeyEncrypted)
|
||||||
|
case packetTypeOnePassSignature:
|
||||||
|
msgErr = sequence.Next(OPSSymbol)
|
||||||
|
p = new(OnePassSignature)
|
||||||
|
case packetTypeCompressed:
|
||||||
|
msgErr = sequence.Next(CompSymbol)
|
||||||
|
p = new(Compressed)
|
||||||
|
case packetTypeSymmetricallyEncrypted:
|
||||||
|
msgErr = sequence.Next(EncSymbol)
|
||||||
|
p = new(SymmetricallyEncrypted)
|
||||||
|
case packetTypeLiteralData:
|
||||||
|
msgErr = sequence.Next(LDSymbol)
|
||||||
|
p = new(LiteralData)
|
||||||
|
case packetTypeSymmetricallyEncryptedIntegrityProtected:
|
||||||
|
msgErr = sequence.Next(EncSymbol)
|
||||||
|
se := new(SymmetricallyEncrypted)
|
||||||
|
se.IntegrityProtected = true
|
||||||
|
p = se
|
||||||
|
case packetTypeAEADEncrypted:
|
||||||
|
msgErr = sequence.Next(EncSymbol)
|
||||||
|
p = new(AEADEncrypted)
|
||||||
|
case packetPadding:
|
||||||
|
p = Padding(len)
|
||||||
|
case packetTypeMarker:
|
||||||
|
p = new(Marker)
|
||||||
|
case packetTypeTrust:
|
||||||
|
// Not implemented, just consume
|
||||||
|
err = errors.UnknownPacketTypeError(tag)
|
||||||
|
case packetTypePrivateKey,
|
||||||
|
packetTypePrivateSubkey,
|
||||||
|
packetTypePublicKey,
|
||||||
|
packetTypePublicSubkey,
|
||||||
|
packetTypeUserId,
|
||||||
|
packetTypeUserAttribute:
|
||||||
|
msgErr = sequence.Next(UnknownSymbol)
|
||||||
|
consumeAll(contents)
|
||||||
|
default:
|
||||||
|
// Packet Tags from 0 to 39 are critical.
|
||||||
|
// Packet Tags from 40 to 63 are non-critical.
|
||||||
|
if tag < 40 {
|
||||||
|
err = errors.CriticalUnknownPacketTypeError(tag)
|
||||||
|
} else {
|
||||||
|
err = errors.UnknownPacketTypeError(tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if p != nil {
|
if p != nil {
|
||||||
err = p.parse(contents)
|
err = p.parse(contents)
|
||||||
|
|
@ -385,17 +473,17 @@ type SignatureType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SigTypeBinary SignatureType = 0x00
|
SigTypeBinary SignatureType = 0x00
|
||||||
SigTypeText = 0x01
|
SigTypeText SignatureType = 0x01
|
||||||
SigTypeGenericCert = 0x10
|
SigTypeGenericCert SignatureType = 0x10
|
||||||
SigTypePersonaCert = 0x11
|
SigTypePersonaCert SignatureType = 0x11
|
||||||
SigTypeCasualCert = 0x12
|
SigTypeCasualCert SignatureType = 0x12
|
||||||
SigTypePositiveCert = 0x13
|
SigTypePositiveCert SignatureType = 0x13
|
||||||
SigTypeSubkeyBinding = 0x18
|
SigTypeSubkeyBinding SignatureType = 0x18
|
||||||
SigTypePrimaryKeyBinding = 0x19
|
SigTypePrimaryKeyBinding SignatureType = 0x19
|
||||||
SigTypeDirectSignature = 0x1F
|
SigTypeDirectSignature SignatureType = 0x1F
|
||||||
SigTypeKeyRevocation = 0x20
|
SigTypeKeyRevocation SignatureType = 0x20
|
||||||
SigTypeSubkeyRevocation = 0x28
|
SigTypeSubkeyRevocation SignatureType = 0x28
|
||||||
SigTypeCertificationRevocation = 0x30
|
SigTypeCertificationRevocation SignatureType = 0x30
|
||||||
)
|
)
|
||||||
|
|
||||||
// PublicKeyAlgorithm represents the different public key system specified for
|
// PublicKeyAlgorithm represents the different public key system specified for
|
||||||
|
|
@ -412,6 +500,11 @@ const (
|
||||||
PubKeyAlgoECDSA PublicKeyAlgorithm = 19
|
PubKeyAlgoECDSA PublicKeyAlgorithm = 19
|
||||||
// https://www.ietf.org/archive/id/draft-koch-eddsa-for-openpgp-04.txt
|
// https://www.ietf.org/archive/id/draft-koch-eddsa-for-openpgp-04.txt
|
||||||
PubKeyAlgoEdDSA PublicKeyAlgorithm = 22
|
PubKeyAlgoEdDSA PublicKeyAlgorithm = 22
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh
|
||||||
|
PubKeyAlgoX25519 PublicKeyAlgorithm = 25
|
||||||
|
PubKeyAlgoX448 PublicKeyAlgorithm = 26
|
||||||
|
PubKeyAlgoEd25519 PublicKeyAlgorithm = 27
|
||||||
|
PubKeyAlgoEd448 PublicKeyAlgorithm = 28
|
||||||
|
|
||||||
// Deprecated in RFC 4880, Section 13.5. Use key flags instead.
|
// Deprecated in RFC 4880, Section 13.5. Use key flags instead.
|
||||||
PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2
|
PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2
|
||||||
|
|
@ -422,7 +515,7 @@ const (
|
||||||
// key of the given type.
|
// key of the given type.
|
||||||
func (pka PublicKeyAlgorithm) CanEncrypt() bool {
|
func (pka PublicKeyAlgorithm) CanEncrypt() bool {
|
||||||
switch pka {
|
switch pka {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -432,7 +525,7 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool {
|
||||||
// sign a message.
|
// sign a message.
|
||||||
func (pka PublicKeyAlgorithm) CanSign() bool {
|
func (pka PublicKeyAlgorithm) CanSign() bool {
|
||||||
switch pka {
|
switch pka {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA:
|
case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -512,6 +605,11 @@ func (mode AEADMode) TagLength() int {
|
||||||
return algorithm.AEADMode(mode).TagLength()
|
return algorithm.AEADMode(mode).TagLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSupported returns true if the aead mode is supported from the library
|
||||||
|
func (mode AEADMode) IsSupported() bool {
|
||||||
|
return algorithm.AEADMode(mode).TagLength() > 0
|
||||||
|
}
|
||||||
|
|
||||||
// new returns a fresh instance of the given mode.
|
// new returns a fresh instance of the given mode.
|
||||||
func (mode AEADMode) new(block cipher.Block) cipher.AEAD {
|
func (mode AEADMode) new(block cipher.Block) cipher.AEAD {
|
||||||
return algorithm.AEADMode(mode).New(block)
|
return algorithm.AEADMode(mode).New(block)
|
||||||
|
|
@ -526,8 +624,17 @@ const (
|
||||||
KeySuperseded ReasonForRevocation = 1
|
KeySuperseded ReasonForRevocation = 1
|
||||||
KeyCompromised ReasonForRevocation = 2
|
KeyCompromised ReasonForRevocation = 2
|
||||||
KeyRetired ReasonForRevocation = 3
|
KeyRetired ReasonForRevocation = 3
|
||||||
|
UserIDNotValid ReasonForRevocation = 32
|
||||||
|
Unknown ReasonForRevocation = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewReasonForRevocation(value byte) ReasonForRevocation {
|
||||||
|
if value < 4 || value == 32 {
|
||||||
|
return ReasonForRevocation(value)
|
||||||
|
}
|
||||||
|
return Unknown
|
||||||
|
}
|
||||||
|
|
||||||
// Curve is a mapping to supported ECC curves for key generation.
|
// Curve is a mapping to supported ECC curves for key generation.
|
||||||
// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#name-curve-specific-wire-formats
|
// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#name-curve-specific-wire-formats
|
||||||
type Curve string
|
type Curve string
|
||||||
|
|
@ -549,3 +656,20 @@ type TrustLevel uint8
|
||||||
|
|
||||||
// TrustAmount represents a trust amount per RFC4880 5.2.3.13
|
// TrustAmount represents a trust amount per RFC4880 5.2.3.13
|
||||||
type TrustAmount uint8
|
type TrustAmount uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// versionSize is the length in bytes of the version value.
|
||||||
|
versionSize = 1
|
||||||
|
// algorithmSize is the length in bytes of the key algorithm value.
|
||||||
|
algorithmSize = 1
|
||||||
|
// keyVersionSize is the length in bytes of the key version value
|
||||||
|
keyVersionSize = 1
|
||||||
|
// keyIdSize is the length in bytes of the key identifier value.
|
||||||
|
keyIdSize = 8
|
||||||
|
// timestampSize is the length in bytes of encoded timestamps.
|
||||||
|
timestampSize = 4
|
||||||
|
// fingerprintSizeV6 is the length in bytes of the key fingerprint in v6.
|
||||||
|
fingerprintSizeV6 = 32
|
||||||
|
// fingerprintSize is the length in bytes of the key fingerprint.
|
||||||
|
fingerprintSize = 20
|
||||||
|
)
|
||||||
|
|
|
||||||
222
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/packet_sequence.go
generated
vendored
Normal file
222
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/packet_sequence.go
generated
vendored
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
package packet
|
||||||
|
|
||||||
|
// This file implements the pushdown automata (PDA) from PGPainless (Paul Schaub)
|
||||||
|
// to verify pgp packet sequences. See Paul's blogpost for more details:
|
||||||
|
// https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewErrMalformedMessage(from State, input InputSymbol, stackSymbol StackSymbol) errors.ErrMalformedMessage {
|
||||||
|
return errors.ErrMalformedMessage(fmt.Sprintf("state %d, input symbol %d, stack symbol %d ", from, input, stackSymbol))
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputSymbol defines the input alphabet of the PDA
|
||||||
|
type InputSymbol uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
LDSymbol InputSymbol = iota
|
||||||
|
SigSymbol
|
||||||
|
OPSSymbol
|
||||||
|
CompSymbol
|
||||||
|
ESKSymbol
|
||||||
|
EncSymbol
|
||||||
|
EOSSymbol
|
||||||
|
UnknownSymbol
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackSymbol defines the stack alphabet of the PDA
|
||||||
|
type StackSymbol int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
MsgStackSymbol StackSymbol = iota
|
||||||
|
OpsStackSymbol
|
||||||
|
KeyStackSymbol
|
||||||
|
EndStackSymbol
|
||||||
|
EmptyStackSymbol
|
||||||
|
)
|
||||||
|
|
||||||
|
// State defines the states of the PDA
|
||||||
|
type State int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpenPGPMessage State = iota
|
||||||
|
ESKMessage
|
||||||
|
LiteralMessage
|
||||||
|
CompressedMessage
|
||||||
|
EncryptedMessage
|
||||||
|
ValidMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
// transition represents a state transition in the PDA
|
||||||
|
type transition func(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error)
|
||||||
|
|
||||||
|
// SequenceVerifier is a pushdown automata to verify
|
||||||
|
// PGP messages packet sequences according to rfc4880.
|
||||||
|
type SequenceVerifier struct {
|
||||||
|
stack []StackSymbol
|
||||||
|
state State
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next performs a state transition with the given input symbol.
|
||||||
|
// If the transition fails a ErrMalformedMessage is returned.
|
||||||
|
func (sv *SequenceVerifier) Next(input InputSymbol) error {
|
||||||
|
for {
|
||||||
|
stackSymbol := sv.popStack()
|
||||||
|
transitionFunc := getTransition(sv.state)
|
||||||
|
nextState, newStackSymbols, redo, err := transitionFunc(input, stackSymbol)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if redo {
|
||||||
|
sv.pushStack(stackSymbol)
|
||||||
|
}
|
||||||
|
for _, newStackSymbol := range newStackSymbols {
|
||||||
|
sv.pushStack(newStackSymbol)
|
||||||
|
}
|
||||||
|
sv.state = nextState
|
||||||
|
if !redo {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid returns true if RDA is in a valid state.
|
||||||
|
func (sv *SequenceVerifier) Valid() bool {
|
||||||
|
return sv.state == ValidMessage && len(sv.stack) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SequenceVerifier) AssertValid() error {
|
||||||
|
if !sv.Valid() {
|
||||||
|
return errors.ErrMalformedMessage("invalid message")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSequenceVerifier() *SequenceVerifier {
|
||||||
|
return &SequenceVerifier{
|
||||||
|
stack: []StackSymbol{EndStackSymbol, MsgStackSymbol},
|
||||||
|
state: OpenPGPMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SequenceVerifier) popStack() StackSymbol {
|
||||||
|
if len(sv.stack) == 0 {
|
||||||
|
return EmptyStackSymbol
|
||||||
|
}
|
||||||
|
elemIndex := len(sv.stack) - 1
|
||||||
|
stackSymbol := sv.stack[elemIndex]
|
||||||
|
sv.stack = sv.stack[:elemIndex]
|
||||||
|
return stackSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SequenceVerifier) pushStack(stackSymbol StackSymbol) {
|
||||||
|
sv.stack = append(sv.stack, stackSymbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransition(from State) transition {
|
||||||
|
switch from {
|
||||||
|
case OpenPGPMessage:
|
||||||
|
return fromOpenPGPMessage
|
||||||
|
case LiteralMessage:
|
||||||
|
return fromLiteralMessage
|
||||||
|
case CompressedMessage:
|
||||||
|
return fromCompressedMessage
|
||||||
|
case EncryptedMessage:
|
||||||
|
return fromEncryptedMessage
|
||||||
|
case ESKMessage:
|
||||||
|
return fromESKMessage
|
||||||
|
case ValidMessage:
|
||||||
|
return fromValidMessage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromOpenPGPMessage is the transition for the state OpenPGPMessage.
|
||||||
|
func fromOpenPGPMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
|
||||||
|
if stackSymbol != MsgStackSymbol {
|
||||||
|
return 0, nil, false, NewErrMalformedMessage(OpenPGPMessage, input, stackSymbol)
|
||||||
|
}
|
||||||
|
switch input {
|
||||||
|
case LDSymbol:
|
||||||
|
return LiteralMessage, nil, false, nil
|
||||||
|
case SigSymbol:
|
||||||
|
return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, false, nil
|
||||||
|
case OPSSymbol:
|
||||||
|
return OpenPGPMessage, []StackSymbol{OpsStackSymbol, MsgStackSymbol}, false, nil
|
||||||
|
case CompSymbol:
|
||||||
|
return CompressedMessage, nil, false, nil
|
||||||
|
case ESKSymbol:
|
||||||
|
return ESKMessage, []StackSymbol{KeyStackSymbol}, false, nil
|
||||||
|
case EncSymbol:
|
||||||
|
return EncryptedMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
return 0, nil, false, NewErrMalformedMessage(OpenPGPMessage, input, stackSymbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromESKMessage is the transition for the state ESKMessage.
|
||||||
|
func fromESKMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
|
||||||
|
if stackSymbol != KeyStackSymbol {
|
||||||
|
return 0, nil, false, NewErrMalformedMessage(ESKMessage, input, stackSymbol)
|
||||||
|
}
|
||||||
|
switch input {
|
||||||
|
case ESKSymbol:
|
||||||
|
return ESKMessage, []StackSymbol{KeyStackSymbol}, false, nil
|
||||||
|
case EncSymbol:
|
||||||
|
return EncryptedMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
return 0, nil, false, NewErrMalformedMessage(ESKMessage, input, stackSymbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromLiteralMessage is the transition for the state LiteralMessage.
|
||||||
|
func fromLiteralMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
|
||||||
|
switch input {
|
||||||
|
case SigSymbol:
|
||||||
|
if stackSymbol == OpsStackSymbol {
|
||||||
|
return LiteralMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
case EOSSymbol:
|
||||||
|
if stackSymbol == EndStackSymbol {
|
||||||
|
return ValidMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, nil, false, NewErrMalformedMessage(LiteralMessage, input, stackSymbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromLiteralMessage is the transition for the state CompressedMessage.
|
||||||
|
func fromCompressedMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
|
||||||
|
switch input {
|
||||||
|
case SigSymbol:
|
||||||
|
if stackSymbol == OpsStackSymbol {
|
||||||
|
return CompressedMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
case EOSSymbol:
|
||||||
|
if stackSymbol == EndStackSymbol {
|
||||||
|
return ValidMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromEncryptedMessage is the transition for the state EncryptedMessage.
|
||||||
|
func fromEncryptedMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
|
||||||
|
switch input {
|
||||||
|
case SigSymbol:
|
||||||
|
if stackSymbol == OpsStackSymbol {
|
||||||
|
return EncryptedMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
case EOSSymbol:
|
||||||
|
if stackSymbol == EndStackSymbol {
|
||||||
|
return ValidMessage, nil, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromValidMessage is the transition for the state ValidMessage.
|
||||||
|
func fromValidMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
|
||||||
|
return 0, nil, false, NewErrMalformedMessage(ValidMessage, input, stackSymbol)
|
||||||
|
}
|
||||||
24
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/packet_unsupported.go
generated
vendored
Normal file
24
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/packet_unsupported.go
generated
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package packet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnsupportedPackage represents a OpenPGP packet with a known packet type
|
||||||
|
// but with unsupported content.
|
||||||
|
type UnsupportedPacket struct {
|
||||||
|
IncompletePacket Packet
|
||||||
|
Error errors.UnsupportedError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements the Packet interface
|
||||||
|
func (up *UnsupportedPacket) parse(read io.Reader) error {
|
||||||
|
err := up.IncompletePacket.parse(read)
|
||||||
|
if castedErr, ok := err.(errors.UnsupportedError); ok {
|
||||||
|
up.Error = castedErr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package packet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Padding type represents a Padding Packet (Tag 21).
|
||||||
|
// The padding type is represented by the length of its padding.
|
||||||
|
// see https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-padding-packet-tag-21
|
||||||
|
type Padding int
|
||||||
|
|
||||||
|
// parse just ignores the padding content.
|
||||||
|
func (pad Padding) parse(reader io.Reader) error {
|
||||||
|
_, err := io.CopyN(io.Discard, reader, int64(pad))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializePadding writes the padding to writer.
|
||||||
|
func (pad Padding) SerializePadding(writer io.Writer, rand io.Reader) error {
|
||||||
|
err := serializeHeader(writer, packetPadding, int(pad))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.CopyN(writer, rand, int64(pad))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -9,22 +9,28 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/dsa"
|
"crypto/dsa"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
|
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
|
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/ed25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/ed448"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
|
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/s2k"
|
"github.com/ProtonMail/go-crypto/openpgp/s2k"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x448"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrivateKey represents a possibly encrypted private key. See RFC 4880,
|
// PrivateKey represents a possibly encrypted private key. See RFC 4880,
|
||||||
|
|
@ -35,14 +41,14 @@ type PrivateKey struct {
|
||||||
encryptedData []byte
|
encryptedData []byte
|
||||||
cipher CipherFunction
|
cipher CipherFunction
|
||||||
s2k func(out, in []byte)
|
s2k func(out, in []byte)
|
||||||
// An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519}.PrivateKey or
|
aead AEADMode // only relevant if S2KAEAD is enabled
|
||||||
|
// An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519|ed448}.PrivateKey or
|
||||||
// crypto.Signer/crypto.Decrypter (Decryptor RSA only).
|
// crypto.Signer/crypto.Decrypter (Decryptor RSA only).
|
||||||
PrivateKey interface{}
|
PrivateKey interface{}
|
||||||
sha1Checksum bool
|
iv []byte
|
||||||
iv []byte
|
|
||||||
|
|
||||||
// Type of encryption of the S2K packet
|
// Type of encryption of the S2K packet
|
||||||
// Allowed values are 0 (Not encrypted), 254 (SHA1), or
|
// Allowed values are 0 (Not encrypted), 253 (AEAD), 254 (SHA1), or
|
||||||
// 255 (2-byte checksum)
|
// 255 (2-byte checksum)
|
||||||
s2kType S2KType
|
s2kType S2KType
|
||||||
// Full parameters of the S2K packet
|
// Full parameters of the S2K packet
|
||||||
|
|
@ -55,6 +61,8 @@ type S2KType uint8
|
||||||
const (
|
const (
|
||||||
// S2KNON unencrypt
|
// S2KNON unencrypt
|
||||||
S2KNON S2KType = 0
|
S2KNON S2KType = 0
|
||||||
|
// S2KAEAD use authenticated encryption
|
||||||
|
S2KAEAD S2KType = 253
|
||||||
// S2KSHA1 sha1 sum check
|
// S2KSHA1 sha1 sum check
|
||||||
S2KSHA1 S2KType = 254
|
S2KSHA1 S2KType = 254
|
||||||
// S2KCHECKSUM sum check
|
// S2KCHECKSUM sum check
|
||||||
|
|
@ -103,6 +111,34 @@ func NewECDHPrivateKey(creationTime time.Time, priv *ecdh.PrivateKey) *PrivateKe
|
||||||
return pk
|
return pk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewX25519PrivateKey(creationTime time.Time, priv *x25519.PrivateKey) *PrivateKey {
|
||||||
|
pk := new(PrivateKey)
|
||||||
|
pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey)
|
||||||
|
pk.PrivateKey = priv
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewX448PrivateKey(creationTime time.Time, priv *x448.PrivateKey) *PrivateKey {
|
||||||
|
pk := new(PrivateKey)
|
||||||
|
pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey)
|
||||||
|
pk.PrivateKey = priv
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEd25519PrivateKey(creationTime time.Time, priv *ed25519.PrivateKey) *PrivateKey {
|
||||||
|
pk := new(PrivateKey)
|
||||||
|
pk.PublicKey = *NewEd25519PublicKey(creationTime, &priv.PublicKey)
|
||||||
|
pk.PrivateKey = priv
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEd448PrivateKey(creationTime time.Time, priv *ed448.PrivateKey) *PrivateKey {
|
||||||
|
pk := new(PrivateKey)
|
||||||
|
pk.PublicKey = *NewEd448PublicKey(creationTime, &priv.PublicKey)
|
||||||
|
pk.PrivateKey = priv
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
// NewSignerPrivateKey creates a PrivateKey from a crypto.Signer that
|
// NewSignerPrivateKey creates a PrivateKey from a crypto.Signer that
|
||||||
// implements RSA, ECDSA or EdDSA.
|
// implements RSA, ECDSA or EdDSA.
|
||||||
func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey {
|
func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey {
|
||||||
|
|
@ -122,6 +158,14 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey
|
||||||
pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey)
|
pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey)
|
||||||
case eddsa.PrivateKey:
|
case eddsa.PrivateKey:
|
||||||
pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey)
|
pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey)
|
||||||
|
case *ed25519.PrivateKey:
|
||||||
|
pk.PublicKey = *NewEd25519PublicKey(creationTime, &pubkey.PublicKey)
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
pk.PublicKey = *NewEd25519PublicKey(creationTime, &pubkey.PublicKey)
|
||||||
|
case *ed448.PrivateKey:
|
||||||
|
pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey)
|
||||||
|
case ed448.PrivateKey:
|
||||||
|
pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey)
|
||||||
default:
|
default:
|
||||||
panic("openpgp: unknown signer type in NewSignerPrivateKey")
|
panic("openpgp: unknown signer type in NewSignerPrivateKey")
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +173,7 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey
|
||||||
return pk
|
return pk
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh}.PrivateKey.
|
// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448}.PrivateKey.
|
||||||
func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey {
|
func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey {
|
||||||
pk := new(PrivateKey)
|
pk := new(PrivateKey)
|
||||||
switch priv := decrypter.(type) {
|
switch priv := decrypter.(type) {
|
||||||
|
|
@ -139,6 +183,10 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv
|
||||||
pk.PublicKey = *NewElGamalPublicKey(creationTime, &priv.PublicKey)
|
pk.PublicKey = *NewElGamalPublicKey(creationTime, &priv.PublicKey)
|
||||||
case *ecdh.PrivateKey:
|
case *ecdh.PrivateKey:
|
||||||
pk.PublicKey = *NewECDHPublicKey(creationTime, &priv.PublicKey)
|
pk.PublicKey = *NewECDHPublicKey(creationTime, &priv.PublicKey)
|
||||||
|
case *x25519.PrivateKey:
|
||||||
|
pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey)
|
||||||
|
case *x448.PrivateKey:
|
||||||
|
pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey)
|
||||||
default:
|
default:
|
||||||
panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey")
|
panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey")
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +200,11 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v5 := pk.PublicKey.Version == 5
|
v5 := pk.PublicKey.Version == 5
|
||||||
|
v6 := pk.PublicKey.Version == 6
|
||||||
|
|
||||||
|
if V5Disabled && v5 {
|
||||||
|
return errors.UnsupportedError("support for parsing v5 entities is disabled; build with `-tags v5` if needed")
|
||||||
|
}
|
||||||
|
|
||||||
var buf [1]byte
|
var buf [1]byte
|
||||||
_, err = readFull(r, buf[:])
|
_, err = readFull(r, buf[:])
|
||||||
|
|
@ -160,7 +213,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
}
|
}
|
||||||
pk.s2kType = S2KType(buf[0])
|
pk.s2kType = S2KType(buf[0])
|
||||||
var optCount [1]byte
|
var optCount [1]byte
|
||||||
if v5 {
|
if v5 || (v6 && pk.s2kType != S2KNON) {
|
||||||
if _, err = readFull(r, optCount[:]); err != nil {
|
if _, err = readFull(r, optCount[:]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -170,9 +223,9 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
case S2KNON:
|
case S2KNON:
|
||||||
pk.s2k = nil
|
pk.s2k = nil
|
||||||
pk.Encrypted = false
|
pk.Encrypted = false
|
||||||
case S2KSHA1, S2KCHECKSUM:
|
case S2KSHA1, S2KCHECKSUM, S2KAEAD:
|
||||||
if v5 && pk.s2kType == S2KCHECKSUM {
|
if (v5 || v6) && pk.s2kType == S2KCHECKSUM {
|
||||||
return errors.StructuralError("wrong s2k identifier for version 5")
|
return errors.StructuralError(fmt.Sprintf("wrong s2k identifier for version %d", pk.Version))
|
||||||
}
|
}
|
||||||
_, err = readFull(r, buf[:])
|
_, err = readFull(r, buf[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -182,6 +235,29 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
if pk.cipher != 0 && !pk.cipher.IsSupported() {
|
if pk.cipher != 0 && !pk.cipher.IsSupported() {
|
||||||
return errors.UnsupportedError("unsupported cipher function in private key")
|
return errors.UnsupportedError("unsupported cipher function in private key")
|
||||||
}
|
}
|
||||||
|
// [Optional] If string-to-key usage octet was 253,
|
||||||
|
// a one-octet AEAD algorithm.
|
||||||
|
if pk.s2kType == S2KAEAD {
|
||||||
|
_, err = readFull(r, buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pk.aead = AEADMode(buf[0])
|
||||||
|
if !pk.aead.IsSupported() {
|
||||||
|
return errors.UnsupportedError("unsupported aead mode in private key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Optional] Only for a version 6 packet,
|
||||||
|
// and if string-to-key usage octet was 255, 254, or 253,
|
||||||
|
// an one-octet count of the following field.
|
||||||
|
if v6 {
|
||||||
|
_, err = readFull(r, buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pk.s2kParams, err = s2k.ParseIntoParams(r)
|
pk.s2kParams, err = s2k.ParseIntoParams(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -189,28 +265,43 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
if pk.s2kParams.Dummy() {
|
if pk.s2kParams.Dummy() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if pk.s2kParams.Mode() == s2k.Argon2S2K && pk.s2kType != S2KAEAD {
|
||||||
|
return errors.StructuralError("using Argon2 S2K without AEAD is not allowed")
|
||||||
|
}
|
||||||
|
if pk.s2kParams.Mode() == s2k.SimpleS2K && pk.Version == 6 {
|
||||||
|
return errors.StructuralError("using Simple S2K with version 6 keys is not allowed")
|
||||||
|
}
|
||||||
pk.s2k, err = pk.s2kParams.Function()
|
pk.s2k, err = pk.s2kParams.Function()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pk.Encrypted = true
|
pk.Encrypted = true
|
||||||
if pk.s2kType == S2KSHA1 {
|
|
||||||
pk.sha1Checksum = true
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return errors.UnsupportedError("deprecated s2k function in private key")
|
return errors.UnsupportedError("deprecated s2k function in private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if pk.Encrypted {
|
if pk.Encrypted {
|
||||||
blockSize := pk.cipher.blockSize()
|
var ivSize int
|
||||||
if blockSize == 0 {
|
// If the S2K usage octet was 253, the IV is of the size expected by the AEAD mode,
|
||||||
|
// unless it's a version 5 key, in which case it's the size of the symmetric cipher's block size.
|
||||||
|
// For all other S2K modes, it's always the block size.
|
||||||
|
if !v5 && pk.s2kType == S2KAEAD {
|
||||||
|
ivSize = pk.aead.IvLength()
|
||||||
|
} else {
|
||||||
|
ivSize = pk.cipher.blockSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ivSize == 0 {
|
||||||
return errors.UnsupportedError("unsupported cipher in private key: " + strconv.Itoa(int(pk.cipher)))
|
return errors.UnsupportedError("unsupported cipher in private key: " + strconv.Itoa(int(pk.cipher)))
|
||||||
}
|
}
|
||||||
pk.iv = make([]byte, blockSize)
|
pk.iv = make([]byte, ivSize)
|
||||||
_, err = readFull(r, pk.iv)
|
_, err = readFull(r, pk.iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if v5 && pk.s2kType == S2KAEAD {
|
||||||
|
pk.iv = pk.iv[:pk.aead.IvLength()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var privateKeyData []byte
|
var privateKeyData []byte
|
||||||
|
|
@ -230,7 +321,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
privateKeyData, err = ioutil.ReadAll(r)
|
privateKeyData, err = io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -239,16 +330,22 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
|
||||||
if len(privateKeyData) < 2 {
|
if len(privateKeyData) < 2 {
|
||||||
return errors.StructuralError("truncated private key data")
|
return errors.StructuralError("truncated private key data")
|
||||||
}
|
}
|
||||||
var sum uint16
|
if pk.Version != 6 {
|
||||||
for i := 0; i < len(privateKeyData)-2; i++ {
|
// checksum
|
||||||
sum += uint16(privateKeyData[i])
|
var sum uint16
|
||||||
|
for i := 0; i < len(privateKeyData)-2; i++ {
|
||||||
|
sum += uint16(privateKeyData[i])
|
||||||
|
}
|
||||||
|
if privateKeyData[len(privateKeyData)-2] != uint8(sum>>8) ||
|
||||||
|
privateKeyData[len(privateKeyData)-1] != uint8(sum) {
|
||||||
|
return errors.StructuralError("private key checksum failure")
|
||||||
|
}
|
||||||
|
privateKeyData = privateKeyData[:len(privateKeyData)-2]
|
||||||
|
return pk.parsePrivateKey(privateKeyData)
|
||||||
|
} else {
|
||||||
|
// No checksum
|
||||||
|
return pk.parsePrivateKey(privateKeyData)
|
||||||
}
|
}
|
||||||
if privateKeyData[len(privateKeyData)-2] != uint8(sum>>8) ||
|
|
||||||
privateKeyData[len(privateKeyData)-1] != uint8(sum) {
|
|
||||||
return errors.StructuralError("private key checksum failure")
|
|
||||||
}
|
|
||||||
privateKeyData = privateKeyData[:len(privateKeyData)-2]
|
|
||||||
return pk.parsePrivateKey(privateKeyData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pk.encryptedData = privateKeyData
|
pk.encryptedData = privateKeyData
|
||||||
|
|
@ -280,18 +377,59 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) {
|
||||||
|
|
||||||
optional := bytes.NewBuffer(nil)
|
optional := bytes.NewBuffer(nil)
|
||||||
if pk.Encrypted || pk.Dummy() {
|
if pk.Encrypted || pk.Dummy() {
|
||||||
optional.Write([]byte{uint8(pk.cipher)})
|
// [Optional] If string-to-key usage octet was 255, 254, or 253,
|
||||||
if err := pk.s2kParams.Serialize(optional); err != nil {
|
// a one-octet symmetric encryption algorithm.
|
||||||
|
if _, err = optional.Write([]byte{uint8(pk.cipher)}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// [Optional] If string-to-key usage octet was 253,
|
||||||
|
// a one-octet AEAD algorithm.
|
||||||
|
if pk.s2kType == S2KAEAD {
|
||||||
|
if _, err = optional.Write([]byte{uint8(pk.aead)}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s2kBuffer := bytes.NewBuffer(nil)
|
||||||
|
if err := pk.s2kParams.Serialize(s2kBuffer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// [Optional] Only for a version 6 packet, and if string-to-key
|
||||||
|
// usage octet was 255, 254, or 253, an one-octet
|
||||||
|
// count of the following field.
|
||||||
|
if pk.Version == 6 {
|
||||||
|
if _, err = optional.Write([]byte{uint8(s2kBuffer.Len())}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [Optional] If string-to-key usage octet was 255, 254, or 253,
|
||||||
|
// a string-to-key (S2K) specifier. The length of the string-to-key specifier
|
||||||
|
// depends on its type
|
||||||
|
if _, err = io.Copy(optional, s2kBuffer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IV
|
||||||
if pk.Encrypted {
|
if pk.Encrypted {
|
||||||
optional.Write(pk.iv)
|
if _, err = optional.Write(pk.iv); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pk.Version == 5 && pk.s2kType == S2KAEAD {
|
||||||
|
// Add padding for version 5
|
||||||
|
padding := make([]byte, pk.cipher.blockSize()-len(pk.iv))
|
||||||
|
if _, err = optional.Write(padding); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pk.Version == 5 {
|
if pk.Version == 5 || (pk.Version == 6 && pk.s2kType != S2KNON) {
|
||||||
contents.Write([]byte{uint8(optional.Len())})
|
contents.Write([]byte{uint8(optional.Len())})
|
||||||
}
|
}
|
||||||
io.Copy(contents, optional)
|
|
||||||
|
if _, err := io.Copy(contents, optional); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if !pk.Dummy() {
|
if !pk.Dummy() {
|
||||||
l := 0
|
l := 0
|
||||||
|
|
@ -303,8 +441,10 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
l = buf.Len()
|
l = buf.Len()
|
||||||
checksum := mod64kHash(buf.Bytes())
|
if pk.Version != 6 {
|
||||||
buf.Write([]byte{byte(checksum >> 8), byte(checksum)})
|
checksum := mod64kHash(buf.Bytes())
|
||||||
|
buf.Write([]byte{byte(checksum >> 8), byte(checksum)})
|
||||||
|
}
|
||||||
priv = buf.Bytes()
|
priv = buf.Bytes()
|
||||||
} else {
|
} else {
|
||||||
priv, l = pk.encryptedData, len(pk.encryptedData)
|
priv, l = pk.encryptedData, len(pk.encryptedData)
|
||||||
|
|
@ -370,6 +510,26 @@ func serializeECDHPrivateKey(w io.Writer, priv *ecdh.PrivateKey) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serializeX25519PrivateKey(w io.Writer, priv *x25519.PrivateKey) error {
|
||||||
|
_, err := w.Write(priv.Secret)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeX448PrivateKey(w io.Writer, priv *x448.PrivateKey) error {
|
||||||
|
_, err := w.Write(priv.Secret)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeEd25519PrivateKey(w io.Writer, priv *ed25519.PrivateKey) error {
|
||||||
|
_, err := w.Write(priv.MarshalByteSecret())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error {
|
||||||
|
_, err := w.Write(priv.MarshalByteSecret())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// decrypt decrypts an encrypted private key using a decryption key.
|
// decrypt decrypts an encrypted private key using a decryption key.
|
||||||
func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
|
func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
|
||||||
if pk.Dummy() {
|
if pk.Dummy() {
|
||||||
|
|
@ -378,37 +538,51 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
|
||||||
if !pk.Encrypted {
|
if !pk.Encrypted {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
block := pk.cipher.new(decryptionKey)
|
block := pk.cipher.new(decryptionKey)
|
||||||
cfb := cipher.NewCFBDecrypter(block, pk.iv)
|
var data []byte
|
||||||
|
switch pk.s2kType {
|
||||||
data := make([]byte, len(pk.encryptedData))
|
case S2KAEAD:
|
||||||
cfb.XORKeyStream(data, pk.encryptedData)
|
aead := pk.aead.new(block)
|
||||||
|
additionalData, err := pk.additionalData()
|
||||||
if pk.sha1Checksum {
|
if err != nil {
|
||||||
if len(data) < sha1.Size {
|
return err
|
||||||
return errors.StructuralError("truncated private key data")
|
|
||||||
}
|
}
|
||||||
h := sha1.New()
|
// Decrypt the encrypted key material with aead
|
||||||
h.Write(data[:len(data)-sha1.Size])
|
data, err = aead.Open(nil, pk.iv, pk.encryptedData, additionalData)
|
||||||
sum := h.Sum(nil)
|
if err != nil {
|
||||||
if !bytes.Equal(sum, data[len(data)-sha1.Size:]) {
|
return err
|
||||||
return errors.StructuralError("private key checksum failure")
|
|
||||||
}
|
}
|
||||||
data = data[:len(data)-sha1.Size]
|
case S2KSHA1, S2KCHECKSUM:
|
||||||
} else {
|
cfb := cipher.NewCFBDecrypter(block, pk.iv)
|
||||||
if len(data) < 2 {
|
data = make([]byte, len(pk.encryptedData))
|
||||||
return errors.StructuralError("truncated private key data")
|
cfb.XORKeyStream(data, pk.encryptedData)
|
||||||
|
if pk.s2kType == S2KSHA1 {
|
||||||
|
if len(data) < sha1.Size {
|
||||||
|
return errors.StructuralError("truncated private key data")
|
||||||
|
}
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write(data[:len(data)-sha1.Size])
|
||||||
|
sum := h.Sum(nil)
|
||||||
|
if !bytes.Equal(sum, data[len(data)-sha1.Size:]) {
|
||||||
|
return errors.StructuralError("private key checksum failure")
|
||||||
|
}
|
||||||
|
data = data[:len(data)-sha1.Size]
|
||||||
|
} else {
|
||||||
|
if len(data) < 2 {
|
||||||
|
return errors.StructuralError("truncated private key data")
|
||||||
|
}
|
||||||
|
var sum uint16
|
||||||
|
for i := 0; i < len(data)-2; i++ {
|
||||||
|
sum += uint16(data[i])
|
||||||
|
}
|
||||||
|
if data[len(data)-2] != uint8(sum>>8) ||
|
||||||
|
data[len(data)-1] != uint8(sum) {
|
||||||
|
return errors.StructuralError("private key checksum failure")
|
||||||
|
}
|
||||||
|
data = data[:len(data)-2]
|
||||||
}
|
}
|
||||||
var sum uint16
|
default:
|
||||||
for i := 0; i < len(data)-2; i++ {
|
return errors.InvalidArgumentError("invalid s2k type")
|
||||||
sum += uint16(data[i])
|
|
||||||
}
|
|
||||||
if data[len(data)-2] != uint8(sum>>8) ||
|
|
||||||
data[len(data)-1] != uint8(sum) {
|
|
||||||
return errors.StructuralError("private key checksum failure")
|
|
||||||
}
|
|
||||||
data = data[:len(data)-2]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := pk.parsePrivateKey(data)
|
err := pk.parsePrivateKey(data)
|
||||||
|
|
@ -424,7 +598,6 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
|
||||||
pk.s2k = nil
|
pk.s2k = nil
|
||||||
pk.Encrypted = false
|
pk.Encrypted = false
|
||||||
pk.encryptedData = nil
|
pk.encryptedData = nil
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,6 +613,9 @@ func (pk *PrivateKey) decryptWithCache(passphrase []byte, keyCache *s2k.Cache) e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if pk.s2kType == S2KAEAD {
|
||||||
|
key = pk.applyHKDF(key)
|
||||||
|
}
|
||||||
return pk.decrypt(key)
|
return pk.decrypt(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,11 +630,14 @@ func (pk *PrivateKey) Decrypt(passphrase []byte) error {
|
||||||
|
|
||||||
key := make([]byte, pk.cipher.KeySize())
|
key := make([]byte, pk.cipher.KeySize())
|
||||||
pk.s2k(key, passphrase)
|
pk.s2k(key, passphrase)
|
||||||
|
if pk.s2kType == S2KAEAD {
|
||||||
|
key = pk.applyHKDF(key)
|
||||||
|
}
|
||||||
return pk.decrypt(key)
|
return pk.decrypt(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptPrivateKeys decrypts all encrypted keys with the given config and passphrase.
|
// DecryptPrivateKeys decrypts all encrypted keys with the given config and passphrase.
|
||||||
// Avoids recomputation of similar s2k key derivations.
|
// Avoids recomputation of similar s2k key derivations.
|
||||||
func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error {
|
func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error {
|
||||||
// Create a cache to avoid recomputation of key derviations for the same passphrase.
|
// Create a cache to avoid recomputation of key derviations for the same passphrase.
|
||||||
s2kCache := &s2k.Cache{}
|
s2kCache := &s2k.Cache{}
|
||||||
|
|
@ -474,7 +653,7 @@ func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// encrypt encrypts an unencrypted private key.
|
// encrypt encrypts an unencrypted private key.
|
||||||
func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction CipherFunction) error {
|
func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, s2kType S2KType, cipherFunction CipherFunction, rand io.Reader) error {
|
||||||
if pk.Dummy() {
|
if pk.Dummy() {
|
||||||
return errors.ErrDummyPrivateKey("dummy key found")
|
return errors.ErrDummyPrivateKey("dummy key found")
|
||||||
}
|
}
|
||||||
|
|
@ -485,7 +664,15 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip
|
||||||
if len(key) != cipherFunction.KeySize() {
|
if len(key) != cipherFunction.KeySize() {
|
||||||
return errors.InvalidArgumentError("supplied encryption key has the wrong size")
|
return errors.InvalidArgumentError("supplied encryption key has the wrong size")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.Mode() == s2k.Argon2S2K && s2kType != S2KAEAD {
|
||||||
|
return errors.InvalidArgumentError("using Argon2 S2K without AEAD is not allowed")
|
||||||
|
}
|
||||||
|
if params.Mode() != s2k.Argon2S2K && params.Mode() != s2k.IteratedSaltedS2K &&
|
||||||
|
params.Mode() != s2k.SaltedS2K { // only allowed for high-entropy passphrases
|
||||||
|
return errors.InvalidArgumentError("insecure S2K mode")
|
||||||
|
}
|
||||||
|
|
||||||
priv := bytes.NewBuffer(nil)
|
priv := bytes.NewBuffer(nil)
|
||||||
err := pk.serializePrivateKey(priv)
|
err := pk.serializePrivateKey(priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -497,35 +684,53 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip
|
||||||
pk.s2k, err = pk.s2kParams.Function()
|
pk.s2k, err = pk.s2kParams.Function()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKeyBytes := priv.Bytes()
|
privateKeyBytes := priv.Bytes()
|
||||||
pk.sha1Checksum = true
|
pk.s2kType = s2kType
|
||||||
block := pk.cipher.new(key)
|
block := pk.cipher.new(key)
|
||||||
pk.iv = make([]byte, pk.cipher.blockSize())
|
switch s2kType {
|
||||||
_, err = rand.Read(pk.iv)
|
case S2KAEAD:
|
||||||
if err != nil {
|
if pk.aead == 0 {
|
||||||
return err
|
return errors.StructuralError("aead mode is not set on key")
|
||||||
}
|
|
||||||
cfb := cipher.NewCFBEncrypter(block, pk.iv)
|
|
||||||
|
|
||||||
if pk.sha1Checksum {
|
|
||||||
pk.s2kType = S2KSHA1
|
|
||||||
h := sha1.New()
|
|
||||||
h.Write(privateKeyBytes)
|
|
||||||
sum := h.Sum(nil)
|
|
||||||
privateKeyBytes = append(privateKeyBytes, sum...)
|
|
||||||
} else {
|
|
||||||
pk.s2kType = S2KCHECKSUM
|
|
||||||
var sum uint16
|
|
||||||
for _, b := range privateKeyBytes {
|
|
||||||
sum += uint16(b)
|
|
||||||
}
|
}
|
||||||
priv.Write([]byte{uint8(sum >> 8), uint8(sum)})
|
aead := pk.aead.new(block)
|
||||||
|
additionalData, err := pk.additionalData()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pk.iv = make([]byte, aead.NonceSize())
|
||||||
|
_, err = io.ReadFull(rand, pk.iv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Decrypt the encrypted key material with aead
|
||||||
|
pk.encryptedData = aead.Seal(nil, pk.iv, privateKeyBytes, additionalData)
|
||||||
|
case S2KSHA1, S2KCHECKSUM:
|
||||||
|
pk.iv = make([]byte, pk.cipher.blockSize())
|
||||||
|
_, err = io.ReadFull(rand, pk.iv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfb := cipher.NewCFBEncrypter(block, pk.iv)
|
||||||
|
if s2kType == S2KSHA1 {
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write(privateKeyBytes)
|
||||||
|
sum := h.Sum(nil)
|
||||||
|
privateKeyBytes = append(privateKeyBytes, sum...)
|
||||||
|
} else {
|
||||||
|
var sum uint16
|
||||||
|
for _, b := range privateKeyBytes {
|
||||||
|
sum += uint16(b)
|
||||||
|
}
|
||||||
|
privateKeyBytes = append(privateKeyBytes, []byte{uint8(sum >> 8), uint8(sum)}...)
|
||||||
|
}
|
||||||
|
pk.encryptedData = make([]byte, len(privateKeyBytes))
|
||||||
|
cfb.XORKeyStream(pk.encryptedData, privateKeyBytes)
|
||||||
|
default:
|
||||||
|
return errors.InvalidArgumentError("invalid s2k type for encryption")
|
||||||
}
|
}
|
||||||
|
|
||||||
pk.encryptedData = make([]byte, len(privateKeyBytes))
|
|
||||||
cfb.XORKeyStream(pk.encryptedData, privateKeyBytes)
|
|
||||||
pk.Encrypted = true
|
pk.Encrypted = true
|
||||||
pk.PrivateKey = nil
|
pk.PrivateKey = nil
|
||||||
return err
|
return err
|
||||||
|
|
@ -544,8 +749,15 @@ func (pk *PrivateKey) EncryptWithConfig(passphrase []byte, config *Config) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s2k(key, passphrase)
|
s2k(key, passphrase)
|
||||||
|
s2kType := S2KSHA1
|
||||||
|
if config.AEAD() != nil {
|
||||||
|
s2kType = S2KAEAD
|
||||||
|
pk.aead = config.AEAD().Mode()
|
||||||
|
pk.cipher = config.Cipher()
|
||||||
|
key = pk.applyHKDF(key)
|
||||||
|
}
|
||||||
// Encrypt the private key with the derived encryption key.
|
// Encrypt the private key with the derived encryption key.
|
||||||
return pk.encrypt(key, params, config.Cipher())
|
return pk.encrypt(key, params, s2kType, config.Cipher(), config.Random())
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptPrivateKeys encrypts all unencrypted keys with the given config and passphrase.
|
// EncryptPrivateKeys encrypts all unencrypted keys with the given config and passphrase.
|
||||||
|
|
@ -564,7 +776,16 @@ func EncryptPrivateKeys(keys []*PrivateKey, passphrase []byte, config *Config) e
|
||||||
s2k(encryptionKey, passphrase)
|
s2k(encryptionKey, passphrase)
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if key != nil && !key.Dummy() && !key.Encrypted {
|
if key != nil && !key.Dummy() && !key.Encrypted {
|
||||||
err = key.encrypt(encryptionKey, params, config.Cipher())
|
s2kType := S2KSHA1
|
||||||
|
if config.AEAD() != nil {
|
||||||
|
s2kType = S2KAEAD
|
||||||
|
key.aead = config.AEAD().Mode()
|
||||||
|
key.cipher = config.Cipher()
|
||||||
|
derivedKey := key.applyHKDF(encryptionKey)
|
||||||
|
err = key.encrypt(derivedKey, params, s2kType, config.Cipher(), config.Random())
|
||||||
|
} else {
|
||||||
|
err = key.encrypt(encryptionKey, params, s2kType, config.Cipher(), config.Random())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -581,7 +802,7 @@ func (pk *PrivateKey) Encrypt(passphrase []byte) error {
|
||||||
S2KMode: s2k.IteratedSaltedS2K,
|
S2KMode: s2k.IteratedSaltedS2K,
|
||||||
S2KCount: 65536,
|
S2KCount: 65536,
|
||||||
Hash: crypto.SHA256,
|
Hash: crypto.SHA256,
|
||||||
} ,
|
},
|
||||||
DefaultCipher: CipherAES256,
|
DefaultCipher: CipherAES256,
|
||||||
}
|
}
|
||||||
return pk.EncryptWithConfig(passphrase, config)
|
return pk.EncryptWithConfig(passphrase, config)
|
||||||
|
|
@ -601,6 +822,14 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) {
|
||||||
err = serializeEdDSAPrivateKey(w, priv)
|
err = serializeEdDSAPrivateKey(w, priv)
|
||||||
case *ecdh.PrivateKey:
|
case *ecdh.PrivateKey:
|
||||||
err = serializeECDHPrivateKey(w, priv)
|
err = serializeECDHPrivateKey(w, priv)
|
||||||
|
case *x25519.PrivateKey:
|
||||||
|
err = serializeX25519PrivateKey(w, priv)
|
||||||
|
case *x448.PrivateKey:
|
||||||
|
err = serializeX448PrivateKey(w, priv)
|
||||||
|
case *ed25519.PrivateKey:
|
||||||
|
err = serializeEd25519PrivateKey(w, priv)
|
||||||
|
case *ed448.PrivateKey:
|
||||||
|
err = serializeEd448PrivateKey(w, priv)
|
||||||
default:
|
default:
|
||||||
err = errors.InvalidArgumentError("unknown private key type")
|
err = errors.InvalidArgumentError("unknown private key type")
|
||||||
}
|
}
|
||||||
|
|
@ -621,8 +850,18 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) {
|
||||||
return pk.parseECDHPrivateKey(data)
|
return pk.parseECDHPrivateKey(data)
|
||||||
case PubKeyAlgoEdDSA:
|
case PubKeyAlgoEdDSA:
|
||||||
return pk.parseEdDSAPrivateKey(data)
|
return pk.parseEdDSAPrivateKey(data)
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
return pk.parseX25519PrivateKey(data)
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
return pk.parseX448PrivateKey(data)
|
||||||
|
case PubKeyAlgoEd25519:
|
||||||
|
return pk.parseEd25519PrivateKey(data)
|
||||||
|
case PubKeyAlgoEd448:
|
||||||
|
return pk.parseEd448PrivateKey(data)
|
||||||
|
default:
|
||||||
|
err = errors.StructuralError("unknown private key type")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
panic("impossible")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *PrivateKey) parseRSAPrivateKey(data []byte) (err error) {
|
func (pk *PrivateKey) parseRSAPrivateKey(data []byte) (err error) {
|
||||||
|
|
@ -743,6 +982,86 @@ func (pk *PrivateKey) parseECDHPrivateKey(data []byte) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pk *PrivateKey) parseX25519PrivateKey(data []byte) (err error) {
|
||||||
|
publicKey := pk.PublicKey.PublicKey.(*x25519.PublicKey)
|
||||||
|
privateKey := x25519.NewPrivateKey(*publicKey)
|
||||||
|
privateKey.PublicKey = *publicKey
|
||||||
|
|
||||||
|
privateKey.Secret = make([]byte, x25519.KeySize)
|
||||||
|
|
||||||
|
if len(data) != x25519.KeySize {
|
||||||
|
err = errors.StructuralError("wrong x25519 key size")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
subtle.ConstantTimeCopy(1, privateKey.Secret, data)
|
||||||
|
if err = x25519.Validate(privateKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pk.PrivateKey = privateKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PrivateKey) parseX448PrivateKey(data []byte) (err error) {
|
||||||
|
publicKey := pk.PublicKey.PublicKey.(*x448.PublicKey)
|
||||||
|
privateKey := x448.NewPrivateKey(*publicKey)
|
||||||
|
privateKey.PublicKey = *publicKey
|
||||||
|
|
||||||
|
privateKey.Secret = make([]byte, x448.KeySize)
|
||||||
|
|
||||||
|
if len(data) != x448.KeySize {
|
||||||
|
err = errors.StructuralError("wrong x448 key size")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
subtle.ConstantTimeCopy(1, privateKey.Secret, data)
|
||||||
|
if err = x448.Validate(privateKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pk.PrivateKey = privateKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PrivateKey) parseEd25519PrivateKey(data []byte) (err error) {
|
||||||
|
publicKey := pk.PublicKey.PublicKey.(*ed25519.PublicKey)
|
||||||
|
privateKey := ed25519.NewPrivateKey(*publicKey)
|
||||||
|
privateKey.PublicKey = *publicKey
|
||||||
|
|
||||||
|
if len(data) != ed25519.SeedSize {
|
||||||
|
err = errors.StructuralError("wrong ed25519 key size")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = privateKey.UnmarshalByteSecret(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ed25519.Validate(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pk.PrivateKey = privateKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PrivateKey) parseEd448PrivateKey(data []byte) (err error) {
|
||||||
|
publicKey := pk.PublicKey.PublicKey.(*ed448.PublicKey)
|
||||||
|
privateKey := ed448.NewPrivateKey(*publicKey)
|
||||||
|
privateKey.PublicKey = *publicKey
|
||||||
|
|
||||||
|
if len(data) != ed448.SeedSize {
|
||||||
|
err = errors.StructuralError("wrong ed448 key size")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = privateKey.UnmarshalByteSecret(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ed448.Validate(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pk.PrivateKey = privateKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) {
|
func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) {
|
||||||
eddsaPub := pk.PublicKey.PublicKey.(*eddsa.PublicKey)
|
eddsaPub := pk.PublicKey.PublicKey.(*eddsa.PublicKey)
|
||||||
eddsaPriv := eddsa.NewPrivateKey(*eddsaPub)
|
eddsaPriv := eddsa.NewPrivateKey(*eddsaPub)
|
||||||
|
|
@ -767,6 +1086,41 @@ func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pk *PrivateKey) additionalData() ([]byte, error) {
|
||||||
|
additionalData := bytes.NewBuffer(nil)
|
||||||
|
// Write additional data prefix based on packet type
|
||||||
|
var packetByte byte
|
||||||
|
if pk.PublicKey.IsSubkey {
|
||||||
|
packetByte = 0xc7
|
||||||
|
} else {
|
||||||
|
packetByte = 0xc5
|
||||||
|
}
|
||||||
|
// Write public key to additional data
|
||||||
|
_, err := additionalData.Write([]byte{packetByte})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = pk.PublicKey.serializeWithoutHeaders(additionalData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return additionalData.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PrivateKey) applyHKDF(inputKey []byte) []byte {
|
||||||
|
var packetByte byte
|
||||||
|
if pk.PublicKey.IsSubkey {
|
||||||
|
packetByte = 0xc7
|
||||||
|
} else {
|
||||||
|
packetByte = 0xc5
|
||||||
|
}
|
||||||
|
associatedData := []byte{packetByte, byte(pk.Version), byte(pk.cipher), byte(pk.aead)}
|
||||||
|
hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData)
|
||||||
|
encryptionKey := make([]byte, pk.cipher.KeySize())
|
||||||
|
_, _ = readFull(hkdfReader, encryptionKey)
|
||||||
|
return encryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
func validateDSAParameters(priv *dsa.PrivateKey) error {
|
func validateDSAParameters(priv *dsa.PrivateKey) error {
|
||||||
p := priv.P // group prime
|
p := priv.P // group prime
|
||||||
q := priv.Q // subgroup order
|
q := priv.Q // subgroup order
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/dsa"
|
"crypto/dsa"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
|
@ -21,23 +20,24 @@ import (
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
|
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
|
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/ed25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/ed448"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
|
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x25519"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/x448"
|
||||||
)
|
)
|
||||||
|
|
||||||
type kdfHashFunction byte
|
|
||||||
type kdfAlgorithm byte
|
|
||||||
|
|
||||||
// PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2.
|
// PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2.
|
||||||
type PublicKey struct {
|
type PublicKey struct {
|
||||||
Version int
|
Version int
|
||||||
CreationTime time.Time
|
CreationTime time.Time
|
||||||
PubKeyAlgo PublicKeyAlgorithm
|
PubKeyAlgo PublicKeyAlgorithm
|
||||||
PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey
|
PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey
|
||||||
Fingerprint []byte
|
Fingerprint []byte
|
||||||
KeyId uint64
|
KeyId uint64
|
||||||
IsSubkey bool
|
IsSubkey bool
|
||||||
|
|
@ -61,11 +61,19 @@ func (pk *PublicKey) UpgradeToV5() {
|
||||||
pk.setFingerprintAndKeyId()
|
pk.setFingerprintAndKeyId()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpgradeToV6 updates the version of the key to v6, and updates all necessary
|
||||||
|
// fields.
|
||||||
|
func (pk *PublicKey) UpgradeToV6() error {
|
||||||
|
pk.Version = 6
|
||||||
|
pk.setFingerprintAndKeyId()
|
||||||
|
return pk.checkV6Compatibility()
|
||||||
|
}
|
||||||
|
|
||||||
// signingKey provides a convenient abstraction over signature verification
|
// signingKey provides a convenient abstraction over signature verification
|
||||||
// for v3 and v4 public keys.
|
// for v3 and v4 public keys.
|
||||||
type signingKey interface {
|
type signingKey interface {
|
||||||
SerializeForHash(io.Writer) error
|
SerializeForHash(io.Writer) error
|
||||||
SerializeSignaturePrefix(io.Writer)
|
SerializeSignaturePrefix(io.Writer) error
|
||||||
serializeWithoutHeaders(io.Writer) error
|
serializeWithoutHeaders(io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,6 +182,54 @@ func NewEdDSAPublicKey(creationTime time.Time, pub *eddsa.PublicKey) *PublicKey
|
||||||
return pk
|
return pk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewX25519PublicKey(creationTime time.Time, pub *x25519.PublicKey) *PublicKey {
|
||||||
|
pk := &PublicKey{
|
||||||
|
Version: 4,
|
||||||
|
CreationTime: creationTime,
|
||||||
|
PubKeyAlgo: PubKeyAlgoX25519,
|
||||||
|
PublicKey: pub,
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.setFingerprintAndKeyId()
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewX448PublicKey(creationTime time.Time, pub *x448.PublicKey) *PublicKey {
|
||||||
|
pk := &PublicKey{
|
||||||
|
Version: 4,
|
||||||
|
CreationTime: creationTime,
|
||||||
|
PubKeyAlgo: PubKeyAlgoX448,
|
||||||
|
PublicKey: pub,
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.setFingerprintAndKeyId()
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEd25519PublicKey(creationTime time.Time, pub *ed25519.PublicKey) *PublicKey {
|
||||||
|
pk := &PublicKey{
|
||||||
|
Version: 4,
|
||||||
|
CreationTime: creationTime,
|
||||||
|
PubKeyAlgo: PubKeyAlgoEd25519,
|
||||||
|
PublicKey: pub,
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.setFingerprintAndKeyId()
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey {
|
||||||
|
pk := &PublicKey{
|
||||||
|
Version: 4,
|
||||||
|
CreationTime: creationTime,
|
||||||
|
PubKeyAlgo: PubKeyAlgoEd448,
|
||||||
|
PublicKey: pub,
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.setFingerprintAndKeyId()
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
func (pk *PublicKey) parse(r io.Reader) (err error) {
|
func (pk *PublicKey) parse(r io.Reader) (err error) {
|
||||||
// RFC 4880, section 5.5.2
|
// RFC 4880, section 5.5.2
|
||||||
var buf [6]byte
|
var buf [6]byte
|
||||||
|
|
@ -181,12 +237,19 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if buf[0] != 4 && buf[0] != 5 {
|
|
||||||
|
pk.Version = int(buf[0])
|
||||||
|
if pk.Version != 4 && pk.Version != 5 && pk.Version != 6 {
|
||||||
return errors.UnsupportedError("public key version " + strconv.Itoa(int(buf[0])))
|
return errors.UnsupportedError("public key version " + strconv.Itoa(int(buf[0])))
|
||||||
}
|
}
|
||||||
|
|
||||||
pk.Version = int(buf[0])
|
if V5Disabled && pk.Version == 5 {
|
||||||
if pk.Version == 5 {
|
return errors.UnsupportedError("support for parsing v5 entities is disabled; build with `-tags v5` if needed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pk.Version >= 5 {
|
||||||
|
// Read the four-octet scalar octet count
|
||||||
|
// The count is not used in this implementation
|
||||||
var n [4]byte
|
var n [4]byte
|
||||||
_, err = readFull(r, n[:])
|
_, err = readFull(r, n[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -195,6 +258,7 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
|
||||||
}
|
}
|
||||||
pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0)
|
pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0)
|
||||||
pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5])
|
pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5])
|
||||||
|
// Ignore four-ocet length
|
||||||
switch pk.PubKeyAlgo {
|
switch pk.PubKeyAlgo {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly:
|
||||||
err = pk.parseRSA(r)
|
err = pk.parseRSA(r)
|
||||||
|
|
@ -208,6 +272,14 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
|
||||||
err = pk.parseECDH(r)
|
err = pk.parseECDH(r)
|
||||||
case PubKeyAlgoEdDSA:
|
case PubKeyAlgoEdDSA:
|
||||||
err = pk.parseEdDSA(r)
|
err = pk.parseEdDSA(r)
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
err = pk.parseX25519(r)
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
err = pk.parseX448(r)
|
||||||
|
case PubKeyAlgoEd25519:
|
||||||
|
err = pk.parseEd25519(r)
|
||||||
|
case PubKeyAlgoEd448:
|
||||||
|
err = pk.parseEd448(r)
|
||||||
default:
|
default:
|
||||||
err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo)))
|
err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo)))
|
||||||
}
|
}
|
||||||
|
|
@ -221,21 +293,44 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
|
||||||
|
|
||||||
func (pk *PublicKey) setFingerprintAndKeyId() {
|
func (pk *PublicKey) setFingerprintAndKeyId() {
|
||||||
// RFC 4880, section 12.2
|
// RFC 4880, section 12.2
|
||||||
if pk.Version == 5 {
|
if pk.Version >= 5 {
|
||||||
fingerprint := sha256.New()
|
fingerprint := sha256.New()
|
||||||
pk.SerializeForHash(fingerprint)
|
if err := pk.SerializeForHash(fingerprint); err != nil {
|
||||||
|
// Should not happen for a hash.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
pk.Fingerprint = make([]byte, 32)
|
pk.Fingerprint = make([]byte, 32)
|
||||||
copy(pk.Fingerprint, fingerprint.Sum(nil))
|
copy(pk.Fingerprint, fingerprint.Sum(nil))
|
||||||
pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[:8])
|
pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[:8])
|
||||||
} else {
|
} else {
|
||||||
fingerprint := sha1.New()
|
fingerprint := sha1.New()
|
||||||
pk.SerializeForHash(fingerprint)
|
if err := pk.SerializeForHash(fingerprint); err != nil {
|
||||||
|
// Should not happen for a hash.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
pk.Fingerprint = make([]byte, 20)
|
pk.Fingerprint = make([]byte, 20)
|
||||||
copy(pk.Fingerprint, fingerprint.Sum(nil))
|
copy(pk.Fingerprint, fingerprint.Sum(nil))
|
||||||
pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[12:20])
|
pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[12:20])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey) checkV6Compatibility() error {
|
||||||
|
// Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs.
|
||||||
|
switch pk.PubKeyAlgo {
|
||||||
|
case PubKeyAlgoECDH:
|
||||||
|
curveInfo := ecc.FindByOid(pk.oid)
|
||||||
|
if curveInfo == nil {
|
||||||
|
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
||||||
|
}
|
||||||
|
if curveInfo.GenName == ecc.Curve25519GenName {
|
||||||
|
return errors.StructuralError("cannot generate v6 key with deprecated OID: Curve25519Legacy")
|
||||||
|
}
|
||||||
|
case PubKeyAlgoEdDSA:
|
||||||
|
return errors.StructuralError("cannot generate v6 key with deprecated algorithm: EdDSALegacy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseRSA parses RSA public key material from the given Reader. See RFC 4880,
|
// parseRSA parses RSA public key material from the given Reader. See RFC 4880,
|
||||||
// section 5.5.2.
|
// section 5.5.2.
|
||||||
func (pk *PublicKey) parseRSA(r io.Reader) (err error) {
|
func (pk *PublicKey) parseRSA(r io.Reader) (err error) {
|
||||||
|
|
@ -324,16 +419,17 @@ func (pk *PublicKey) parseECDSA(r io.Reader) (err error) {
|
||||||
if _, err = pk.oid.ReadFrom(r); err != nil {
|
if _, err = pk.oid.ReadFrom(r); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pk.p = new(encoding.MPI)
|
|
||||||
if _, err = pk.p.ReadFrom(r); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
curveInfo := ecc.FindByOid(pk.oid)
|
curveInfo := ecc.FindByOid(pk.oid)
|
||||||
if curveInfo == nil {
|
if curveInfo == nil {
|
||||||
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pk.p = new(encoding.MPI)
|
||||||
|
if _, err = pk.p.ReadFrom(r); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c, ok := curveInfo.Curve.(ecc.ECDSACurve)
|
c, ok := curveInfo.Curve.(ecc.ECDSACurve)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
|
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
|
||||||
|
|
@ -353,6 +449,17 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
|
||||||
if _, err = pk.oid.ReadFrom(r); err != nil {
|
if _, err = pk.oid.ReadFrom(r); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
curveInfo := ecc.FindByOid(pk.oid)
|
||||||
|
if curveInfo == nil {
|
||||||
|
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if pk.Version == 6 && curveInfo.GenName == ecc.Curve25519GenName {
|
||||||
|
// Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs.
|
||||||
|
return errors.StructuralError("cannot read v6 key with deprecated OID: Curve25519Legacy")
|
||||||
|
}
|
||||||
|
|
||||||
pk.p = new(encoding.MPI)
|
pk.p = new(encoding.MPI)
|
||||||
if _, err = pk.p.ReadFrom(r); err != nil {
|
if _, err = pk.p.ReadFrom(r); err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -362,12 +469,6 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
curveInfo := ecc.FindByOid(pk.oid)
|
|
||||||
|
|
||||||
if curveInfo == nil {
|
|
||||||
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
|
||||||
}
|
|
||||||
|
|
||||||
c, ok := curveInfo.Curve.(ecc.ECDHCurve)
|
c, ok := curveInfo.Curve.(ecc.ECDHCurve)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
|
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
|
||||||
|
|
@ -396,10 +497,16 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) {
|
func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) {
|
||||||
|
if pk.Version == 6 {
|
||||||
|
// Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs.
|
||||||
|
return errors.StructuralError("cannot generate v6 key with deprecated algorithm: EdDSALegacy")
|
||||||
|
}
|
||||||
|
|
||||||
pk.oid = new(encoding.OID)
|
pk.oid = new(encoding.OID)
|
||||||
if _, err = pk.oid.ReadFrom(r); err != nil {
|
if _, err = pk.oid.ReadFrom(r); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
curveInfo := ecc.FindByOid(pk.oid)
|
curveInfo := ecc.FindByOid(pk.oid)
|
||||||
if curveInfo == nil {
|
if curveInfo == nil {
|
||||||
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
||||||
|
|
@ -435,75 +542,145 @@ func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey) parseX25519(r io.Reader) (err error) {
|
||||||
|
point := make([]byte, x25519.KeySize)
|
||||||
|
_, err = io.ReadFull(r, point)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pub := &x25519.PublicKey{
|
||||||
|
Point: point,
|
||||||
|
}
|
||||||
|
pk.PublicKey = pub
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey) parseX448(r io.Reader) (err error) {
|
||||||
|
point := make([]byte, x448.KeySize)
|
||||||
|
_, err = io.ReadFull(r, point)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pub := &x448.PublicKey{
|
||||||
|
Point: point,
|
||||||
|
}
|
||||||
|
pk.PublicKey = pub
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey) parseEd25519(r io.Reader) (err error) {
|
||||||
|
point := make([]byte, ed25519.PublicKeySize)
|
||||||
|
_, err = io.ReadFull(r, point)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pub := &ed25519.PublicKey{
|
||||||
|
Point: point,
|
||||||
|
}
|
||||||
|
pk.PublicKey = pub
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey) parseEd448(r io.Reader) (err error) {
|
||||||
|
point := make([]byte, ed448.PublicKeySize)
|
||||||
|
_, err = io.ReadFull(r, point)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pub := &ed448.PublicKey{
|
||||||
|
Point: point,
|
||||||
|
}
|
||||||
|
pk.PublicKey = pub
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// SerializeForHash serializes the PublicKey to w with the special packet
|
// SerializeForHash serializes the PublicKey to w with the special packet
|
||||||
// header format needed for hashing.
|
// header format needed for hashing.
|
||||||
func (pk *PublicKey) SerializeForHash(w io.Writer) error {
|
func (pk *PublicKey) SerializeForHash(w io.Writer) error {
|
||||||
pk.SerializeSignaturePrefix(w)
|
if err := pk.SerializeSignaturePrefix(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return pk.serializeWithoutHeaders(w)
|
return pk.serializeWithoutHeaders(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerializeSignaturePrefix writes the prefix for this public key to the given Writer.
|
// SerializeSignaturePrefix writes the prefix for this public key to the given Writer.
|
||||||
// The prefix is used when calculating a signature over this public key. See
|
// The prefix is used when calculating a signature over this public key. See
|
||||||
// RFC 4880, section 5.2.4.
|
// RFC 4880, section 5.2.4.
|
||||||
func (pk *PublicKey) SerializeSignaturePrefix(w io.Writer) {
|
func (pk *PublicKey) SerializeSignaturePrefix(w io.Writer) error {
|
||||||
var pLength = pk.algorithmSpecificByteCount()
|
var pLength = pk.algorithmSpecificByteCount()
|
||||||
if pk.Version == 5 {
|
// version, timestamp, algorithm
|
||||||
pLength += 10 // version, timestamp (4), algorithm, key octet count (4).
|
pLength += versionSize + timestampSize + algorithmSize
|
||||||
w.Write([]byte{
|
if pk.Version >= 5 {
|
||||||
0x9A,
|
// key octet count (4).
|
||||||
|
pLength += 4
|
||||||
|
_, err := w.Write([]byte{
|
||||||
|
// When a v4 signature is made over a key, the hash data starts with the octet 0x99, followed by a two-octet length
|
||||||
|
// of the key, and then the body of the key packet. When a v6 signature is made over a key, the hash data starts
|
||||||
|
// with the salt, then octet 0x9B, followed by a four-octet length of the key, and then the body of the key packet.
|
||||||
|
0x95 + byte(pk.Version),
|
||||||
byte(pLength >> 24),
|
byte(pLength >> 24),
|
||||||
byte(pLength >> 16),
|
byte(pLength >> 16),
|
||||||
byte(pLength >> 8),
|
byte(pLength >> 8),
|
||||||
byte(pLength),
|
byte(pLength),
|
||||||
})
|
})
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
pLength += 6
|
if _, err := w.Write([]byte{0x99, byte(pLength >> 8), byte(pLength)}); err != nil {
|
||||||
w.Write([]byte{0x99, byte(pLength >> 8), byte(pLength)})
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *PublicKey) Serialize(w io.Writer) (err error) {
|
func (pk *PublicKey) Serialize(w io.Writer) (err error) {
|
||||||
length := 6 // 6 byte header
|
length := uint32(versionSize + timestampSize + algorithmSize) // 6 byte header
|
||||||
length += pk.algorithmSpecificByteCount()
|
length += pk.algorithmSpecificByteCount()
|
||||||
if pk.Version == 5 {
|
if pk.Version >= 5 {
|
||||||
length += 4 // octet key count
|
length += 4 // octet key count
|
||||||
}
|
}
|
||||||
packetType := packetTypePublicKey
|
packetType := packetTypePublicKey
|
||||||
if pk.IsSubkey {
|
if pk.IsSubkey {
|
||||||
packetType = packetTypePublicSubkey
|
packetType = packetTypePublicSubkey
|
||||||
}
|
}
|
||||||
err = serializeHeader(w, packetType, length)
|
err = serializeHeader(w, packetType, int(length))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return pk.serializeWithoutHeaders(w)
|
return pk.serializeWithoutHeaders(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *PublicKey) algorithmSpecificByteCount() int {
|
func (pk *PublicKey) algorithmSpecificByteCount() uint32 {
|
||||||
length := 0
|
length := uint32(0)
|
||||||
switch pk.PubKeyAlgo {
|
switch pk.PubKeyAlgo {
|
||||||
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly:
|
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly:
|
||||||
length += int(pk.n.EncodedLength())
|
length += uint32(pk.n.EncodedLength())
|
||||||
length += int(pk.e.EncodedLength())
|
length += uint32(pk.e.EncodedLength())
|
||||||
case PubKeyAlgoDSA:
|
case PubKeyAlgoDSA:
|
||||||
length += int(pk.p.EncodedLength())
|
length += uint32(pk.p.EncodedLength())
|
||||||
length += int(pk.q.EncodedLength())
|
length += uint32(pk.q.EncodedLength())
|
||||||
length += int(pk.g.EncodedLength())
|
length += uint32(pk.g.EncodedLength())
|
||||||
length += int(pk.y.EncodedLength())
|
length += uint32(pk.y.EncodedLength())
|
||||||
case PubKeyAlgoElGamal:
|
case PubKeyAlgoElGamal:
|
||||||
length += int(pk.p.EncodedLength())
|
length += uint32(pk.p.EncodedLength())
|
||||||
length += int(pk.g.EncodedLength())
|
length += uint32(pk.g.EncodedLength())
|
||||||
length += int(pk.y.EncodedLength())
|
length += uint32(pk.y.EncodedLength())
|
||||||
case PubKeyAlgoECDSA:
|
case PubKeyAlgoECDSA:
|
||||||
length += int(pk.oid.EncodedLength())
|
length += uint32(pk.oid.EncodedLength())
|
||||||
length += int(pk.p.EncodedLength())
|
length += uint32(pk.p.EncodedLength())
|
||||||
case PubKeyAlgoECDH:
|
case PubKeyAlgoECDH:
|
||||||
length += int(pk.oid.EncodedLength())
|
length += uint32(pk.oid.EncodedLength())
|
||||||
length += int(pk.p.EncodedLength())
|
length += uint32(pk.p.EncodedLength())
|
||||||
length += int(pk.kdf.EncodedLength())
|
length += uint32(pk.kdf.EncodedLength())
|
||||||
case PubKeyAlgoEdDSA:
|
case PubKeyAlgoEdDSA:
|
||||||
length += int(pk.oid.EncodedLength())
|
length += uint32(pk.oid.EncodedLength())
|
||||||
length += int(pk.p.EncodedLength())
|
length += uint32(pk.p.EncodedLength())
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
length += x25519.KeySize
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
length += x448.KeySize
|
||||||
|
case PubKeyAlgoEd25519:
|
||||||
|
length += ed25519.PublicKeySize
|
||||||
|
case PubKeyAlgoEd448:
|
||||||
|
length += ed448.PublicKeySize
|
||||||
default:
|
default:
|
||||||
panic("unknown public key algorithm")
|
panic("unknown public key algorithm")
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +699,7 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if pk.Version == 5 {
|
if pk.Version >= 5 {
|
||||||
n := pk.algorithmSpecificByteCount()
|
n := pk.algorithmSpecificByteCount()
|
||||||
if _, err = w.Write([]byte{
|
if _, err = w.Write([]byte{
|
||||||
byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),
|
byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),
|
||||||
|
|
@ -580,6 +757,22 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) {
|
||||||
}
|
}
|
||||||
_, err = w.Write(pk.p.EncodedBytes())
|
_, err = w.Write(pk.p.EncodedBytes())
|
||||||
return
|
return
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
publicKey := pk.PublicKey.(*x25519.PublicKey)
|
||||||
|
_, err = w.Write(publicKey.Point)
|
||||||
|
return
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
publicKey := pk.PublicKey.(*x448.PublicKey)
|
||||||
|
_, err = w.Write(publicKey.Point)
|
||||||
|
return
|
||||||
|
case PubKeyAlgoEd25519:
|
||||||
|
publicKey := pk.PublicKey.(*ed25519.PublicKey)
|
||||||
|
_, err = w.Write(publicKey.Point)
|
||||||
|
return
|
||||||
|
case PubKeyAlgoEd448:
|
||||||
|
publicKey := pk.PublicKey.(*ed448.PublicKey)
|
||||||
|
_, err = w.Write(publicKey.Point)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return errors.InvalidArgumentError("bad public-key algorithm")
|
return errors.InvalidArgumentError("bad public-key algorithm")
|
||||||
}
|
}
|
||||||
|
|
@ -589,6 +782,20 @@ func (pk *PublicKey) CanSign() bool {
|
||||||
return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH
|
return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyHashTag returns nil iff sig appears to be a plausible signature of the data
|
||||||
|
// hashed into signed, based solely on its HashTag. signed is mutated by this call.
|
||||||
|
func VerifyHashTag(signed hash.Hash, sig *Signature) (err error) {
|
||||||
|
if sig.Version == 5 && (sig.SigType == 0x00 || sig.SigType == 0x01) {
|
||||||
|
sig.AddMetadataToHashSuffix()
|
||||||
|
}
|
||||||
|
signed.Write(sig.HashSuffix)
|
||||||
|
hashBytes := signed.Sum(nil)
|
||||||
|
if hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1] {
|
||||||
|
return errors.SignatureError("hash tag doesn't match")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// VerifySignature returns nil iff sig is a valid signature, made by this
|
// VerifySignature returns nil iff sig is a valid signature, made by this
|
||||||
// public key, of the data hashed into signed. signed is mutated by this call.
|
// public key, of the data hashed into signed. signed is mutated by this call.
|
||||||
func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err error) {
|
func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err error) {
|
||||||
|
|
@ -600,7 +807,8 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
|
||||||
}
|
}
|
||||||
signed.Write(sig.HashSuffix)
|
signed.Write(sig.HashSuffix)
|
||||||
hashBytes := signed.Sum(nil)
|
hashBytes := signed.Sum(nil)
|
||||||
if sig.Version == 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) {
|
// see discussion https://github.com/ProtonMail/go-crypto/issues/107
|
||||||
|
if sig.Version >= 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) {
|
||||||
return errors.SignatureError("hash tag doesn't match")
|
return errors.SignatureError("hash tag doesn't match")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -639,6 +847,18 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
|
||||||
return errors.SignatureError("EdDSA verification failure")
|
return errors.SignatureError("EdDSA verification failure")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
case PubKeyAlgoEd25519:
|
||||||
|
ed25519PublicKey := pk.PublicKey.(*ed25519.PublicKey)
|
||||||
|
if !ed25519.Verify(ed25519PublicKey, hashBytes, sig.EdSig) {
|
||||||
|
return errors.SignatureError("Ed25519 verification failure")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case PubKeyAlgoEd448:
|
||||||
|
ed448PublicKey := pk.PublicKey.(*ed448.PublicKey)
|
||||||
|
if !ed448.Verify(ed448PublicKey, hashBytes, sig.EdSig) {
|
||||||
|
return errors.SignatureError("ed448 verification failure")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return errors.SignatureError("Unsupported public key algorithm used in signature")
|
return errors.SignatureError("Unsupported public key algorithm used in signature")
|
||||||
}
|
}
|
||||||
|
|
@ -646,11 +866,8 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
|
||||||
|
|
||||||
// keySignatureHash returns a Hash of the message that needs to be signed for
|
// keySignatureHash returns a Hash of the message that needs to be signed for
|
||||||
// pk to assert a subkey relationship to signed.
|
// pk to assert a subkey relationship to signed.
|
||||||
func keySignatureHash(pk, signed signingKey, hashFunc crypto.Hash) (h hash.Hash, err error) {
|
func keySignatureHash(pk, signed signingKey, hashFunc hash.Hash) (h hash.Hash, err error) {
|
||||||
if !hashFunc.Available() {
|
h = hashFunc
|
||||||
return nil, errors.UnsupportedError("hash function")
|
|
||||||
}
|
|
||||||
h = hashFunc.New()
|
|
||||||
|
|
||||||
// RFC 4880, section 5.2.4
|
// RFC 4880, section 5.2.4
|
||||||
err = pk.SerializeForHash(h)
|
err = pk.SerializeForHash(h)
|
||||||
|
|
@ -662,10 +879,28 @@ func keySignatureHash(pk, signed signingKey, hashFunc crypto.Hash) (h hash.Hash,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyKeyHashTag returns nil iff sig appears to be a plausible signature over this
|
||||||
|
// primary key and subkey, based solely on its HashTag.
|
||||||
|
func (pk *PublicKey) VerifyKeyHashTag(signed *PublicKey, sig *Signature) error {
|
||||||
|
preparedHash, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h, err := keySignatureHash(pk, signed, preparedHash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return VerifyHashTag(h, sig)
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyKeySignature returns nil iff sig is a valid signature, made by this
|
// VerifyKeySignature returns nil iff sig is a valid signature, made by this
|
||||||
// public key, of signed.
|
// public key, of signed.
|
||||||
func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error {
|
func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error {
|
||||||
h, err := keySignatureHash(pk, signed, sig.Hash)
|
preparedHash, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h, err := keySignatureHash(pk, signed, preparedHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -679,10 +914,14 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error
|
||||||
if sig.EmbeddedSignature == nil {
|
if sig.EmbeddedSignature == nil {
|
||||||
return errors.StructuralError("signing subkey is missing cross-signature")
|
return errors.StructuralError("signing subkey is missing cross-signature")
|
||||||
}
|
}
|
||||||
|
preparedHashEmbedded, err := sig.EmbeddedSignature.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Verify the cross-signature. This is calculated over the same
|
// Verify the cross-signature. This is calculated over the same
|
||||||
// data as the main signature, so we cannot just recursively
|
// data as the main signature, so we cannot just recursively
|
||||||
// call signed.VerifyKeySignature(...)
|
// call signed.VerifyKeySignature(...)
|
||||||
if h, err = keySignatureHash(pk, signed, sig.EmbeddedSignature.Hash); err != nil {
|
if h, err = keySignatureHash(pk, signed, preparedHashEmbedded); err != nil {
|
||||||
return errors.StructuralError("error while hashing for cross-signature: " + err.Error())
|
return errors.StructuralError("error while hashing for cross-signature: " + err.Error())
|
||||||
}
|
}
|
||||||
if err := signed.VerifySignature(h, sig.EmbeddedSignature); err != nil {
|
if err := signed.VerifySignature(h, sig.EmbeddedSignature); err != nil {
|
||||||
|
|
@ -693,32 +932,44 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyRevocationHash(pk signingKey, hashFunc crypto.Hash) (h hash.Hash, err error) {
|
func keyRevocationHash(pk signingKey, hashFunc hash.Hash) (err error) {
|
||||||
if !hashFunc.Available() {
|
return pk.SerializeForHash(hashFunc)
|
||||||
return nil, errors.UnsupportedError("hash function")
|
}
|
||||||
|
|
||||||
|
// VerifyRevocationHashTag returns nil iff sig appears to be a plausible signature
|
||||||
|
// over this public key, based solely on its HashTag.
|
||||||
|
func (pk *PublicKey) VerifyRevocationHashTag(sig *Signature) (err error) {
|
||||||
|
preparedHash, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
h = hashFunc.New()
|
if err = keyRevocationHash(pk, preparedHash); err != nil {
|
||||||
|
return err
|
||||||
// RFC 4880, section 5.2.4
|
}
|
||||||
err = pk.SerializeForHash(h)
|
return VerifyHashTag(preparedHash, sig)
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyRevocationSignature returns nil iff sig is a valid signature, made by this
|
// VerifyRevocationSignature returns nil iff sig is a valid signature, made by this
|
||||||
// public key.
|
// public key.
|
||||||
func (pk *PublicKey) VerifyRevocationSignature(sig *Signature) (err error) {
|
func (pk *PublicKey) VerifyRevocationSignature(sig *Signature) (err error) {
|
||||||
h, err := keyRevocationHash(pk, sig.Hash)
|
preparedHash, err := sig.PrepareVerify()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return pk.VerifySignature(h, sig)
|
if err = keyRevocationHash(pk, preparedHash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pk.VerifySignature(preparedHash, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifySubkeyRevocationSignature returns nil iff sig is a valid subkey revocation signature,
|
// VerifySubkeyRevocationSignature returns nil iff sig is a valid subkey revocation signature,
|
||||||
// made by this public key, of signed.
|
// made by this public key, of signed.
|
||||||
func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *PublicKey) (err error) {
|
func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *PublicKey) (err error) {
|
||||||
h, err := keySignatureHash(pk, signed, sig.Hash)
|
preparedHash, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h, err := keySignatureHash(pk, signed, preparedHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -727,15 +978,15 @@ func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *Pub
|
||||||
|
|
||||||
// userIdSignatureHash returns a Hash of the message that needs to be signed
|
// userIdSignatureHash returns a Hash of the message that needs to be signed
|
||||||
// to assert that pk is a valid key for id.
|
// to assert that pk is a valid key for id.
|
||||||
func userIdSignatureHash(id string, pk *PublicKey, hashFunc crypto.Hash) (h hash.Hash, err error) {
|
func userIdSignatureHash(id string, pk *PublicKey, h hash.Hash) (err error) {
|
||||||
if !hashFunc.Available() {
|
|
||||||
return nil, errors.UnsupportedError("hash function")
|
|
||||||
}
|
|
||||||
h = hashFunc.New()
|
|
||||||
|
|
||||||
// RFC 4880, section 5.2.4
|
// RFC 4880, section 5.2.4
|
||||||
pk.SerializeSignaturePrefix(h)
|
if err := pk.SerializeSignaturePrefix(h); err != nil {
|
||||||
pk.serializeWithoutHeaders(h)
|
return err
|
||||||
|
}
|
||||||
|
if err := pk.serializeWithoutHeaders(h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var buf [5]byte
|
var buf [5]byte
|
||||||
buf[0] = 0xb4
|
buf[0] = 0xb4
|
||||||
|
|
@ -746,16 +997,51 @@ func userIdSignatureHash(id string, pk *PublicKey, hashFunc crypto.Hash) (h hash
|
||||||
h.Write(buf[:])
|
h.Write(buf[:])
|
||||||
h.Write([]byte(id))
|
h.Write([]byte(id))
|
||||||
|
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// directKeySignatureHash returns a Hash of the message that needs to be signed.
|
||||||
|
func directKeySignatureHash(pk *PublicKey, h hash.Hash) (err error) {
|
||||||
|
return pk.SerializeForHash(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyUserIdHashTag returns nil iff sig appears to be a plausible signature over this
|
||||||
|
// public key and UserId, based solely on its HashTag
|
||||||
|
func (pk *PublicKey) VerifyUserIdHashTag(id string, sig *Signature) (err error) {
|
||||||
|
preparedHash, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = userIdSignatureHash(id, pk, preparedHash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return VerifyHashTag(preparedHash, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyUserIdSignature returns nil iff sig is a valid signature, made by this
|
// VerifyUserIdSignature returns nil iff sig is a valid signature, made by this
|
||||||
// public key, that id is the identity of pub.
|
// public key, that id is the identity of pub.
|
||||||
func (pk *PublicKey) VerifyUserIdSignature(id string, pub *PublicKey, sig *Signature) (err error) {
|
func (pk *PublicKey) VerifyUserIdSignature(id string, pub *PublicKey, sig *Signature) (err error) {
|
||||||
h, err := userIdSignatureHash(id, pub, sig.Hash)
|
h, err := sig.PrepareVerify()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := userIdSignatureHash(id, pub, h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pk.VerifySignature(h, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyDirectKeySignature returns nil iff sig is a valid signature, made by this
|
||||||
|
// public key.
|
||||||
|
func (pk *PublicKey) VerifyDirectKeySignature(sig *Signature) (err error) {
|
||||||
|
h, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := directKeySignatureHash(pk, h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return pk.VerifySignature(h, sig)
|
return pk.VerifySignature(h, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -786,21 +1072,49 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) {
|
||||||
bitLength = pk.p.BitLength()
|
bitLength = pk.p.BitLength()
|
||||||
case PubKeyAlgoEdDSA:
|
case PubKeyAlgoEdDSA:
|
||||||
bitLength = pk.p.BitLength()
|
bitLength = pk.p.BitLength()
|
||||||
|
case PubKeyAlgoX25519:
|
||||||
|
bitLength = x25519.KeySize * 8
|
||||||
|
case PubKeyAlgoX448:
|
||||||
|
bitLength = x448.KeySize * 8
|
||||||
|
case PubKeyAlgoEd25519:
|
||||||
|
bitLength = ed25519.PublicKeySize * 8
|
||||||
|
case PubKeyAlgoEd448:
|
||||||
|
bitLength = ed448.PublicKeySize * 8
|
||||||
default:
|
default:
|
||||||
err = errors.InvalidArgumentError("bad public-key algorithm")
|
err = errors.InvalidArgumentError("bad public-key algorithm")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Curve returns the used elliptic curve of this public key.
|
||||||
|
// Returns an error if no elliptic curve is used.
|
||||||
|
func (pk *PublicKey) Curve() (curve Curve, err error) {
|
||||||
|
switch pk.PubKeyAlgo {
|
||||||
|
case PubKeyAlgoECDSA, PubKeyAlgoECDH, PubKeyAlgoEdDSA:
|
||||||
|
curveInfo := ecc.FindByOid(pk.oid)
|
||||||
|
if curveInfo == nil {
|
||||||
|
return "", errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
|
||||||
|
}
|
||||||
|
curve = Curve(curveInfo.GenName)
|
||||||
|
case PubKeyAlgoEd25519, PubKeyAlgoX25519:
|
||||||
|
curve = Curve25519
|
||||||
|
case PubKeyAlgoEd448, PubKeyAlgoX448:
|
||||||
|
curve = Curve448
|
||||||
|
default:
|
||||||
|
err = errors.InvalidArgumentError("public key does not operate with an elliptic curve")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// KeyExpired returns whether sig is a self-signature of a key that has
|
// KeyExpired returns whether sig is a self-signature of a key that has
|
||||||
// expired or is created in the future.
|
// expired or is created in the future.
|
||||||
func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool {
|
func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool {
|
||||||
if pk.CreationTime.After(currentTime) {
|
if pk.CreationTime.Unix() > currentTime.Unix() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if sig.KeyLifetimeSecs == nil || *sig.KeyLifetimeSecs == 0 {
|
if sig.KeyLifetimeSecs == nil || *sig.KeyLifetimeSecs == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second)
|
expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second)
|
||||||
return currentTime.After(expiry)
|
return currentTime.Unix() > expiry.Unix()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PacketReader interface {
|
||||||
|
Next() (p Packet, err error)
|
||||||
|
Push(reader io.Reader) (err error)
|
||||||
|
Unread(p Packet)
|
||||||
|
}
|
||||||
|
|
||||||
// Reader reads packets from an io.Reader and allows packets to be 'unread' so
|
// Reader reads packets from an io.Reader and allows packets to be 'unread' so
|
||||||
// that they result from the next call to Next.
|
// that they result from the next call to Next.
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
|
|
@ -26,37 +32,81 @@ type Reader struct {
|
||||||
const maxReaders = 32
|
const maxReaders = 32
|
||||||
|
|
||||||
// Next returns the most recently unread Packet, or reads another packet from
|
// Next returns the most recently unread Packet, or reads another packet from
|
||||||
// the top-most io.Reader. Unknown packet types are skipped.
|
// the top-most io.Reader. Unknown/unsupported/Marker packet types are skipped.
|
||||||
func (r *Reader) Next() (p Packet, err error) {
|
func (r *Reader) Next() (p Packet, err error) {
|
||||||
|
for {
|
||||||
|
p, err := r.read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
if _, ok := err.(errors.UnknownPacketTypeError); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := err.(errors.UnsupportedError); ok {
|
||||||
|
switch p.(type) {
|
||||||
|
case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
//A marker packet MUST be ignored when received
|
||||||
|
switch p.(type) {
|
||||||
|
case *Marker:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the most recently unread Packet, or reads another packet from
|
||||||
|
// the top-most io.Reader. Unknown/Marker packet types are skipped while unsupported
|
||||||
|
// packets are returned as UnsupportedPacket type.
|
||||||
|
func (r *Reader) NextWithUnsupported() (p Packet, err error) {
|
||||||
|
for {
|
||||||
|
p, err = r.read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
if _, ok := err.(errors.UnknownPacketTypeError); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if casteErr, ok := err.(errors.UnsupportedError); ok {
|
||||||
|
return &UnsupportedPacket{
|
||||||
|
IncompletePacket: p,
|
||||||
|
Error: casteErr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
//A marker packet MUST be ignored when received
|
||||||
|
switch p.(type) {
|
||||||
|
case *Marker:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) read() (p Packet, err error) {
|
||||||
if len(r.q) > 0 {
|
if len(r.q) > 0 {
|
||||||
p = r.q[len(r.q)-1]
|
p = r.q[len(r.q)-1]
|
||||||
r.q = r.q[:len(r.q)-1]
|
r.q = r.q[:len(r.q)-1]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for len(r.readers) > 0 {
|
for len(r.readers) > 0 {
|
||||||
p, err = Read(r.readers[len(r.readers)-1])
|
p, err = Read(r.readers[len(r.readers)-1])
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
r.readers = r.readers[:len(r.readers)-1]
|
r.readers = r.readers[:len(r.readers)-1]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// TODO: Add strict mode that rejects unknown packets, instead of ignoring them.
|
return p, err
|
||||||
if _, ok := err.(errors.UnknownPacketTypeError); ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := err.(errors.UnsupportedError); ok {
|
|
||||||
switch p.(type) {
|
|
||||||
case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, io.EOF
|
return nil, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,3 +134,76 @@ func NewReader(r io.Reader) *Reader {
|
||||||
readers: []io.Reader{r},
|
readers: []io.Reader{r},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckReader is similar to Reader but additionally
|
||||||
|
// uses the pushdown automata to verify the read packet sequence.
|
||||||
|
type CheckReader struct {
|
||||||
|
Reader
|
||||||
|
verifier *SequenceVerifier
|
||||||
|
fullyRead bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the most recently unread Packet, or reads another packet from
|
||||||
|
// the top-most io.Reader. Unknown packet types are skipped.
|
||||||
|
// If the read packet sequence does not conform to the packet composition
|
||||||
|
// rules in rfc4880, it returns an error.
|
||||||
|
func (r *CheckReader) Next() (p Packet, err error) {
|
||||||
|
if r.fullyRead {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
if len(r.q) > 0 {
|
||||||
|
p = r.q[len(r.q)-1]
|
||||||
|
r.q = r.q[:len(r.q)-1]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var errMsg error
|
||||||
|
for len(r.readers) > 0 {
|
||||||
|
p, errMsg, err = ReadWithCheck(r.readers[len(r.readers)-1], r.verifier)
|
||||||
|
if errMsg != nil {
|
||||||
|
err = errMsg
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
r.readers = r.readers[:len(r.readers)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//A marker packet MUST be ignored when received
|
||||||
|
switch p.(type) {
|
||||||
|
case *Marker:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := err.(errors.UnknownPacketTypeError); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := err.(errors.UnsupportedError); ok {
|
||||||
|
switch p.(type) {
|
||||||
|
case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if errMsg = r.verifier.Next(EOSSymbol); errMsg != nil {
|
||||||
|
return nil, errMsg
|
||||||
|
}
|
||||||
|
if errMsg = r.verifier.AssertValid(); errMsg != nil {
|
||||||
|
return nil, errMsg
|
||||||
|
}
|
||||||
|
r.fullyRead = true
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckReader(r io.Reader) *CheckReader {
|
||||||
|
return &CheckReader{
|
||||||
|
Reader: Reader{
|
||||||
|
q: nil,
|
||||||
|
readers: []io.Reader{r},
|
||||||
|
},
|
||||||
|
verifier: NewSequenceVerifier(),
|
||||||
|
fullyRead: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
15
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/recipient.go
generated
vendored
Normal file
15
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/recipient.go
generated
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package packet
|
||||||
|
|
||||||
|
// Recipient type represents a Intended Recipient Fingerprint subpacket
|
||||||
|
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-intended-recipient-fingerpr
|
||||||
|
type Recipient struct {
|
||||||
|
KeyVersion int
|
||||||
|
Fingerprint []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recipient) Serialize() []byte {
|
||||||
|
packet := make([]byte, len(r.Fingerprint)+1)
|
||||||
|
packet[0] = byte(r.KeyVersion)
|
||||||
|
copy(packet[1:], r.Fingerprint)
|
||||||
|
return packet
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
89
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetric_key_encrypted.go
generated
vendored
89
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetric_key_encrypted.go
generated
vendored
|
|
@ -7,11 +7,13 @@ package packet
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/s2k"
|
"github.com/ProtonMail/go-crypto/openpgp/s2k"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is the largest session key that we'll support. Since at most 256-bit cipher
|
// This is the largest session key that we'll support. Since at most 256-bit cipher
|
||||||
|
|
@ -39,10 +41,21 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ske.Version = int(buf[0])
|
ske.Version = int(buf[0])
|
||||||
if ske.Version != 4 && ske.Version != 5 {
|
if ske.Version != 4 && ske.Version != 5 && ske.Version != 6 {
|
||||||
return errors.UnsupportedError("unknown SymmetricKeyEncrypted version")
|
return errors.UnsupportedError("unknown SymmetricKeyEncrypted version")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if V5Disabled && ske.Version == 5 {
|
||||||
|
return errors.UnsupportedError("support for parsing v5 entities is disabled; build with `-tags v5` if needed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ske.Version > 5 {
|
||||||
|
// Scalar octet count
|
||||||
|
if _, err := readFull(r, buf[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cipher function
|
// Cipher function
|
||||||
if _, err := readFull(r, buf[:]); err != nil {
|
if _, err := readFull(r, buf[:]); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -52,7 +65,7 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
|
||||||
return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[0])))
|
return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[0])))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ske.Version == 5 {
|
if ske.Version >= 5 {
|
||||||
// AEAD mode
|
// AEAD mode
|
||||||
if _, err := readFull(r, buf[:]); err != nil {
|
if _, err := readFull(r, buf[:]); err != nil {
|
||||||
return errors.StructuralError("cannot read AEAD octet from packet")
|
return errors.StructuralError("cannot read AEAD octet from packet")
|
||||||
|
|
@ -60,6 +73,13 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
|
||||||
ske.Mode = AEADMode(buf[0])
|
ske.Mode = AEADMode(buf[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ske.Version > 5 {
|
||||||
|
// Scalar octet count
|
||||||
|
if _, err := readFull(r, buf[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if ske.s2k, err = s2k.Parse(r); err != nil {
|
if ske.s2k, err = s2k.Parse(r); err != nil {
|
||||||
if _, ok := err.(errors.ErrDummyPrivateKey); ok {
|
if _, ok := err.(errors.ErrDummyPrivateKey); ok {
|
||||||
|
|
@ -68,7 +88,7 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ske.Version == 5 {
|
if ske.Version >= 5 {
|
||||||
// AEAD IV
|
// AEAD IV
|
||||||
iv := make([]byte, ske.Mode.IvLength())
|
iv := make([]byte, ske.Mode.IvLength())
|
||||||
_, err := readFull(r, iv)
|
_, err := readFull(r, iv)
|
||||||
|
|
@ -109,8 +129,8 @@ func (ske *SymmetricKeyEncrypted) Decrypt(passphrase []byte) ([]byte, CipherFunc
|
||||||
case 4:
|
case 4:
|
||||||
plaintextKey, cipherFunc, err := ske.decryptV4(key)
|
plaintextKey, cipherFunc, err := ske.decryptV4(key)
|
||||||
return plaintextKey, cipherFunc, err
|
return plaintextKey, cipherFunc, err
|
||||||
case 5:
|
case 5, 6:
|
||||||
plaintextKey, err := ske.decryptV5(key)
|
plaintextKey, err := ske.aeadDecrypt(ske.Version, key)
|
||||||
return plaintextKey, CipherFunction(0), err
|
return plaintextKey, CipherFunction(0), err
|
||||||
}
|
}
|
||||||
err := errors.UnsupportedError("unknown SymmetricKeyEncrypted version")
|
err := errors.UnsupportedError("unknown SymmetricKeyEncrypted version")
|
||||||
|
|
@ -136,9 +156,9 @@ func (ske *SymmetricKeyEncrypted) decryptV4(key []byte) ([]byte, CipherFunction,
|
||||||
return plaintextKey, cipherFunc, nil
|
return plaintextKey, cipherFunc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) {
|
func (ske *SymmetricKeyEncrypted) aeadDecrypt(version int, key []byte) ([]byte, error) {
|
||||||
adata := []byte{0xc3, byte(5), byte(ske.CipherFunc), byte(ske.Mode)}
|
adata := []byte{0xc3, byte(version), byte(ske.CipherFunc), byte(ske.Mode)}
|
||||||
aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata)
|
aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata, version)
|
||||||
|
|
||||||
plaintextKey, err := aead.Open(nil, ske.iv, ske.encryptedKey, adata)
|
plaintextKey, err := aead.Open(nil, ske.iv, ske.encryptedKey, adata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -175,10 +195,22 @@ func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Conf
|
||||||
// the given passphrase. The returned session key must be passed to
|
// the given passphrase. The returned session key must be passed to
|
||||||
// SerializeSymmetricallyEncrypted.
|
// SerializeSymmetricallyEncrypted.
|
||||||
// If config is nil, sensible defaults will be used.
|
// If config is nil, sensible defaults will be used.
|
||||||
|
// Deprecated: Use SerializeSymmetricKeyEncryptedAEADReuseKey instead.
|
||||||
func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, config *Config) (err error) {
|
func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, config *Config) (err error) {
|
||||||
|
return SerializeSymmetricKeyEncryptedAEADReuseKey(w, sessionKey, passphrase, config.AEAD() != nil, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeSymmetricKeyEncryptedAEADReuseKey serializes a symmetric key packet to w.
|
||||||
|
// The packet contains the given session key, encrypted by a key derived from
|
||||||
|
// the given passphrase. The returned session key must be passed to
|
||||||
|
// SerializeSymmetricallyEncrypted.
|
||||||
|
// If aeadSupported is set, SKESK v6 is used, otherwise v4.
|
||||||
|
// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted.
|
||||||
|
// If config is nil, sensible defaults will be used.
|
||||||
|
func SerializeSymmetricKeyEncryptedAEADReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, aeadSupported bool, config *Config) (err error) {
|
||||||
var version int
|
var version int
|
||||||
if config.AEAD() != nil {
|
if aeadSupported {
|
||||||
version = 5
|
version = 6
|
||||||
} else {
|
} else {
|
||||||
version = 4
|
version = 4
|
||||||
}
|
}
|
||||||
|
|
@ -203,11 +235,15 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
|
||||||
switch version {
|
switch version {
|
||||||
case 4:
|
case 4:
|
||||||
packetLength = 2 /* header */ + len(s2kBytes) + 1 /* cipher type */ + keySize
|
packetLength = 2 /* header */ + len(s2kBytes) + 1 /* cipher type */ + keySize
|
||||||
case 5:
|
case 5, 6:
|
||||||
ivLen := config.AEAD().Mode().IvLength()
|
ivLen := config.AEAD().Mode().IvLength()
|
||||||
tagLen := config.AEAD().Mode().TagLength()
|
tagLen := config.AEAD().Mode().TagLength()
|
||||||
packetLength = 3 + len(s2kBytes) + ivLen + keySize + tagLen
|
packetLength = 3 + len(s2kBytes) + ivLen + keySize + tagLen
|
||||||
}
|
}
|
||||||
|
if version > 5 {
|
||||||
|
packetLength += 2 // additional octet count fields
|
||||||
|
}
|
||||||
|
|
||||||
err = serializeHeader(w, packetTypeSymmetricKeyEncrypted, packetLength)
|
err = serializeHeader(w, packetTypeSymmetricKeyEncrypted, packetLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -216,13 +252,22 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
|
||||||
// Symmetric Key Encrypted Version
|
// Symmetric Key Encrypted Version
|
||||||
buf := []byte{byte(version)}
|
buf := []byte{byte(version)}
|
||||||
|
|
||||||
|
if version > 5 {
|
||||||
|
// Scalar octet count
|
||||||
|
buf = append(buf, byte(3+len(s2kBytes)+config.AEAD().Mode().IvLength()))
|
||||||
|
}
|
||||||
|
|
||||||
// Cipher function
|
// Cipher function
|
||||||
buf = append(buf, byte(cipherFunc))
|
buf = append(buf, byte(cipherFunc))
|
||||||
|
|
||||||
if version == 5 {
|
if version >= 5 {
|
||||||
// AEAD mode
|
// AEAD mode
|
||||||
buf = append(buf, byte(config.AEAD().Mode()))
|
buf = append(buf, byte(config.AEAD().Mode()))
|
||||||
}
|
}
|
||||||
|
if version > 5 {
|
||||||
|
// Scalar octet count
|
||||||
|
buf = append(buf, byte(len(s2kBytes)))
|
||||||
|
}
|
||||||
_, err = w.Write(buf)
|
_, err = w.Write(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -243,10 +288,10 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case 5:
|
case 5, 6:
|
||||||
mode := config.AEAD().Mode()
|
mode := config.AEAD().Mode()
|
||||||
adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)}
|
adata := []byte{0xc3, byte(version), byte(cipherFunc), byte(mode)}
|
||||||
aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata)
|
aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata, version)
|
||||||
|
|
||||||
// Sample iv using random reader
|
// Sample iv using random reader
|
||||||
iv := make([]byte, config.AEAD().Mode().IvLength())
|
iv := make([]byte, config.AEAD().Mode().IvLength())
|
||||||
|
|
@ -270,7 +315,17 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte) (aead cipher.AEAD) {
|
func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte, version int) (aead cipher.AEAD) {
|
||||||
blockCipher := c.new(inputKey)
|
var blockCipher cipher.Block
|
||||||
|
if version > 5 {
|
||||||
|
hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData)
|
||||||
|
|
||||||
|
encryptionKey := make([]byte, c.KeySize())
|
||||||
|
_, _ = readFull(hkdfReader, encryptionKey)
|
||||||
|
|
||||||
|
blockCipher = c.new(encryptionKey)
|
||||||
|
} else {
|
||||||
|
blockCipher = c.new(inputKey)
|
||||||
|
}
|
||||||
return mode.new(blockCipher)
|
return mode.new(blockCipher)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetrically_encrypted.go
generated
vendored
4
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetrically_encrypted.go
generated
vendored
|
|
@ -74,6 +74,10 @@ func (se *SymmetricallyEncrypted) Decrypt(c CipherFunction, key []byte) (io.Read
|
||||||
// SerializeSymmetricallyEncrypted serializes a symmetrically encrypted packet
|
// SerializeSymmetricallyEncrypted serializes a symmetrically encrypted packet
|
||||||
// to w and returns a WriteCloser to which the to-be-encrypted packets can be
|
// to w and returns a WriteCloser to which the to-be-encrypted packets can be
|
||||||
// written.
|
// written.
|
||||||
|
// If aeadSupported is set to true, SEIPDv2 is used with the indicated CipherSuite.
|
||||||
|
// Otherwise, SEIPDv1 is used with the indicated CipherFunction.
|
||||||
|
// Note: aeadSupported MUST match the value passed to SerializeEncryptedKeyAEAD
|
||||||
|
// and/or SerializeSymmetricKeyEncryptedAEADReuseKey.
|
||||||
// If config is nil, sensible defaults will be used.
|
// If config is nil, sensible defaults will be used.
|
||||||
func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, aeadSupported bool, cipherSuite CipherSuite, key []byte, config *Config) (Contents io.WriteCloser, err error) {
|
func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, aeadSupported bool, cipherSuite CipherSuite, key []byte, config *Config) (Contents io.WriteCloser, err error) {
|
||||||
writeCloser := noOpCloser{w}
|
writeCloser := noOpCloser{w}
|
||||||
|
|
|
||||||
15
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetrically_encrypted_aead.go
generated
vendored
15
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetrically_encrypted_aead.go
generated
vendored
|
|
@ -7,7 +7,9 @@ package packet
|
||||||
import (
|
import (
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
|
|
@ -25,19 +27,19 @@ func (se *SymmetricallyEncrypted) parseAead(r io.Reader) error {
|
||||||
se.Cipher = CipherFunction(headerData[0])
|
se.Cipher = CipherFunction(headerData[0])
|
||||||
// cipherFunc must have block size 16 to use AEAD
|
// cipherFunc must have block size 16 to use AEAD
|
||||||
if se.Cipher.blockSize() != 16 {
|
if se.Cipher.blockSize() != 16 {
|
||||||
return errors.UnsupportedError("invalid aead cipher: " + string(se.Cipher))
|
return errors.UnsupportedError("invalid aead cipher: " + strconv.Itoa(int(se.Cipher)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
se.Mode = AEADMode(headerData[1])
|
se.Mode = AEADMode(headerData[1])
|
||||||
if se.Mode.TagLength() == 0 {
|
if se.Mode.TagLength() == 0 {
|
||||||
return errors.UnsupportedError("unknown aead mode: " + string(se.Mode))
|
return errors.UnsupportedError("unknown aead mode: " + strconv.Itoa(int(se.Mode)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk size
|
// Chunk size
|
||||||
se.ChunkSizeByte = headerData[2]
|
se.ChunkSizeByte = headerData[2]
|
||||||
if se.ChunkSizeByte > 16 {
|
if se.ChunkSizeByte > 16 {
|
||||||
return errors.UnsupportedError("invalid aead chunk size byte: " + string(se.ChunkSizeByte))
|
return errors.UnsupportedError("invalid aead chunk size byte: " + strconv.Itoa(int(se.ChunkSizeByte)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Salt
|
// Salt
|
||||||
|
|
@ -62,8 +64,11 @@ func (se *SymmetricallyEncrypted) associatedData() []byte {
|
||||||
// decryptAead decrypts a V2 SEIPD packet (AEAD) as specified in
|
// decryptAead decrypts a V2 SEIPD packet (AEAD) as specified in
|
||||||
// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2
|
// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2
|
||||||
func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, error) {
|
func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, error) {
|
||||||
aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData())
|
if se.Cipher.KeySize() != len(inputKey) {
|
||||||
|
return nil, errors.StructuralError(fmt.Sprintf("invalid session key length for cipher: got %d bytes, but expected %d bytes", len(inputKey), se.Cipher.KeySize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData())
|
||||||
// Carry the first tagLen bytes
|
// Carry the first tagLen bytes
|
||||||
tagLen := se.Mode.TagLength()
|
tagLen := se.Mode.TagLength()
|
||||||
peekedBytes := make([]byte, tagLen)
|
peekedBytes := make([]byte, tagLen)
|
||||||
|
|
@ -115,7 +120,7 @@ func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite
|
||||||
|
|
||||||
// Random salt
|
// Random salt
|
||||||
salt := make([]byte, aeadSaltSize)
|
salt := make([]byte, aeadSaltSize)
|
||||||
if _, err := rand.Read(salt); err != nil {
|
if _, err := io.ReadFull(rand, salt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetrically_encrypted_mdc.go
generated
vendored
10
vendor/github.com/ProtonMail/go-crypto/openpgp/packet/symmetrically_encrypted_mdc.go
generated
vendored
|
|
@ -148,7 +148,7 @@ const mdcPacketTagByte = byte(0x80) | 0x40 | 19
|
||||||
|
|
||||||
func (ser *seMDCReader) Close() error {
|
func (ser *seMDCReader) Close() error {
|
||||||
if ser.error {
|
if ser.error {
|
||||||
return errors.ErrMDCMissing
|
return errors.ErrMDCHashMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
for !ser.eof {
|
for !ser.eof {
|
||||||
|
|
@ -159,7 +159,7 @@ func (ser *seMDCReader) Close() error {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ErrMDCMissing
|
return errors.ErrMDCHashMismatch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ func (ser *seMDCReader) Close() error {
|
||||||
// The hash already includes the MDC header, but we still check its value
|
// The hash already includes the MDC header, but we still check its value
|
||||||
// to confirm encryption correctness
|
// to confirm encryption correctness
|
||||||
if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size {
|
if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size {
|
||||||
return errors.ErrMDCMissing
|
return errors.ErrMDCHashMismatch
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -237,9 +237,9 @@ func serializeSymmetricallyEncryptedMdc(ciphertext io.WriteCloser, c CipherFunct
|
||||||
block := c.new(key)
|
block := c.new(key)
|
||||||
blockSize := block.BlockSize()
|
blockSize := block.BlockSize()
|
||||||
iv := make([]byte, blockSize)
|
iv := make([]byte, blockSize)
|
||||||
_, err = config.Random().Read(iv)
|
_, err = io.ReadFull(config.Random(), iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync)
|
s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync)
|
||||||
_, err = ciphertext.Write(prefix)
|
_, err = ciphertext.Write(prefix)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const UserAttrImageSubpacket = 1
|
const UserAttrImageSubpacket = 1
|
||||||
|
|
@ -63,7 +62,7 @@ func NewUserAttribute(contents ...*OpaqueSubpacket) *UserAttribute {
|
||||||
|
|
||||||
func (uat *UserAttribute) parse(r io.Reader) (err error) {
|
func (uat *UserAttribute) parse(r io.Reader) (err error) {
|
||||||
// RFC 4880, section 5.13
|
// RFC 4880, section 5.13
|
||||||
b, err := ioutil.ReadAll(r)
|
b, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -66,7 +65,7 @@ func NewUserId(name, comment, email string) *UserId {
|
||||||
|
|
||||||
func (uid *UserId) parse(r io.Reader) (err error) {
|
func (uid *UserId) parse(r io.Reader) (err error) {
|
||||||
// RFC 4880, section 5.11
|
// RFC 4880, section 5.11
|
||||||
b, err := ioutil.ReadAll(r)
|
b, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ type MessageDetails struct {
|
||||||
DecryptedWith Key // the private key used to decrypt the message, if any.
|
DecryptedWith Key // the private key used to decrypt the message, if any.
|
||||||
IsSigned bool // true if the message is signed.
|
IsSigned bool // true if the message is signed.
|
||||||
SignedByKeyId uint64 // the key id of the signer, if any.
|
SignedByKeyId uint64 // the key id of the signer, if any.
|
||||||
|
SignedByFingerprint []byte // the key fingerprint of the signer, if any.
|
||||||
SignedBy *Key // the key of the signer, if available.
|
SignedBy *Key // the key of the signer, if available.
|
||||||
LiteralData *packet.LiteralData // the metadata of the contents
|
LiteralData *packet.LiteralData // the metadata of the contents
|
||||||
UnverifiedBody io.Reader // the contents of the message.
|
UnverifiedBody io.Reader // the contents of the message.
|
||||||
|
|
@ -117,7 +118,7 @@ ParsePackets:
|
||||||
// This packet contains the decryption key encrypted to a public key.
|
// This packet contains the decryption key encrypted to a public key.
|
||||||
md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId)
|
md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId)
|
||||||
switch p.Algo {
|
switch p.Algo {
|
||||||
case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH:
|
case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
|
|
@ -232,7 +233,7 @@ FindKey:
|
||||||
}
|
}
|
||||||
mdFinal, sensitiveParsingErr := readSignedMessage(packets, md, keyring, config)
|
mdFinal, sensitiveParsingErr := readSignedMessage(packets, md, keyring, config)
|
||||||
if sensitiveParsingErr != nil {
|
if sensitiveParsingErr != nil {
|
||||||
return nil, errors.StructuralError("parsing error")
|
return nil, errors.HandleSensitiveParsingError(sensitiveParsingErr, md.decrypted != nil)
|
||||||
}
|
}
|
||||||
return mdFinal, nil
|
return mdFinal, nil
|
||||||
}
|
}
|
||||||
|
|
@ -270,13 +271,17 @@ FindLiteralData:
|
||||||
prevLast = true
|
prevLast = true
|
||||||
}
|
}
|
||||||
|
|
||||||
h, wrappedHash, err = hashForSignature(p.Hash, p.SigType)
|
h, wrappedHash, err = hashForSignature(p.Hash, p.SigType, p.Salt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
md.SignatureError = err
|
md.SignatureError = err
|
||||||
}
|
}
|
||||||
|
|
||||||
md.IsSigned = true
|
md.IsSigned = true
|
||||||
|
if p.Version == 6 {
|
||||||
|
md.SignedByFingerprint = p.KeyFingerprint
|
||||||
|
}
|
||||||
md.SignedByKeyId = p.KeyId
|
md.SignedByKeyId = p.KeyId
|
||||||
|
|
||||||
if keyring != nil {
|
if keyring != nil {
|
||||||
keys := keyring.KeysByIdUsage(p.KeyId, packet.KeyFlagSign)
|
keys := keyring.KeysByIdUsage(p.KeyId, packet.KeyFlagSign)
|
||||||
if len(keys) > 0 {
|
if len(keys) > 0 {
|
||||||
|
|
@ -292,7 +297,7 @@ FindLiteralData:
|
||||||
if md.IsSigned && md.SignatureError == nil {
|
if md.IsSigned && md.SignatureError == nil {
|
||||||
md.UnverifiedBody = &signatureCheckReader{packets, h, wrappedHash, md, config}
|
md.UnverifiedBody = &signatureCheckReader{packets, h, wrappedHash, md, config}
|
||||||
} else if md.decrypted != nil {
|
} else if md.decrypted != nil {
|
||||||
md.UnverifiedBody = checkReader{md}
|
md.UnverifiedBody = &checkReader{md, false}
|
||||||
} else {
|
} else {
|
||||||
md.UnverifiedBody = md.LiteralData.Body
|
md.UnverifiedBody = md.LiteralData.Body
|
||||||
}
|
}
|
||||||
|
|
@ -300,12 +305,22 @@ FindLiteralData:
|
||||||
return md, nil
|
return md, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapHashForSignature(hashFunc hash.Hash, sigType packet.SignatureType) (hash.Hash, error) {
|
||||||
|
switch sigType {
|
||||||
|
case packet.SigTypeBinary:
|
||||||
|
return hashFunc, nil
|
||||||
|
case packet.SigTypeText:
|
||||||
|
return NewCanonicalTextHash(hashFunc), nil
|
||||||
|
}
|
||||||
|
return nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType)))
|
||||||
|
}
|
||||||
|
|
||||||
// hashForSignature returns a pair of hashes that can be used to verify a
|
// hashForSignature returns a pair of hashes that can be used to verify a
|
||||||
// signature. The signature may specify that the contents of the signed message
|
// signature. The signature may specify that the contents of the signed message
|
||||||
// should be preprocessed (i.e. to normalize line endings). Thus this function
|
// should be preprocessed (i.e. to normalize line endings). Thus this function
|
||||||
// returns two hashes. The second should be used to hash the message itself and
|
// returns two hashes. The second should be used to hash the message itself and
|
||||||
// performs any needed preprocessing.
|
// performs any needed preprocessing.
|
||||||
func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) {
|
func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType, sigSalt []byte) (hash.Hash, hash.Hash, error) {
|
||||||
if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok {
|
if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok {
|
||||||
return nil, nil, errors.UnsupportedError("unsupported hash function")
|
return nil, nil, errors.UnsupportedError("unsupported hash function")
|
||||||
}
|
}
|
||||||
|
|
@ -313,14 +328,19 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.
|
||||||
return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc)))
|
return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc)))
|
||||||
}
|
}
|
||||||
h := hashFunc.New()
|
h := hashFunc.New()
|
||||||
|
if sigSalt != nil {
|
||||||
|
h.Write(sigSalt)
|
||||||
|
}
|
||||||
|
wrappedHash, err := wrapHashForSignature(h, sigType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
switch sigType {
|
switch sigType {
|
||||||
case packet.SigTypeBinary:
|
case packet.SigTypeBinary:
|
||||||
return h, h, nil
|
return h, wrappedHash, nil
|
||||||
case packet.SigTypeText:
|
case packet.SigTypeText:
|
||||||
return h, NewCanonicalTextHash(h), nil
|
return h, wrappedHash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType)))
|
return nil, nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,21 +348,27 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.
|
||||||
// it closes the ReadCloser from any SymmetricallyEncrypted packet to trigger
|
// it closes the ReadCloser from any SymmetricallyEncrypted packet to trigger
|
||||||
// MDC checks.
|
// MDC checks.
|
||||||
type checkReader struct {
|
type checkReader struct {
|
||||||
md *MessageDetails
|
md *MessageDetails
|
||||||
|
checked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr checkReader) Read(buf []byte) (int, error) {
|
func (cr *checkReader) Read(buf []byte) (int, error) {
|
||||||
n, sensitiveParsingError := cr.md.LiteralData.Body.Read(buf)
|
n, sensitiveParsingError := cr.md.LiteralData.Body.Read(buf)
|
||||||
if sensitiveParsingError == io.EOF {
|
if sensitiveParsingError == io.EOF {
|
||||||
|
if cr.checked {
|
||||||
|
// Only check once
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
mdcErr := cr.md.decrypted.Close()
|
mdcErr := cr.md.decrypted.Close()
|
||||||
if mdcErr != nil {
|
if mdcErr != nil {
|
||||||
return n, mdcErr
|
return n, mdcErr
|
||||||
}
|
}
|
||||||
|
cr.checked = true
|
||||||
return n, io.EOF
|
return n, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
if sensitiveParsingError != nil {
|
if sensitiveParsingError != nil {
|
||||||
return n, errors.StructuralError("parsing error")
|
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
|
|
@ -366,6 +392,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
|
||||||
scr.wrappedHash.Write(buf[:n])
|
scr.wrappedHash.Write(buf[:n])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readsDecryptedData := scr.md.decrypted != nil
|
||||||
if sensitiveParsingError == io.EOF {
|
if sensitiveParsingError == io.EOF {
|
||||||
var p packet.Packet
|
var p packet.Packet
|
||||||
var readError error
|
var readError error
|
||||||
|
|
@ -384,7 +411,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
|
||||||
key := scr.md.SignedBy
|
key := scr.md.SignedBy
|
||||||
signatureError := key.PublicKey.VerifySignature(scr.h, sig)
|
signatureError := key.PublicKey.VerifySignature(scr.h, sig)
|
||||||
if signatureError == nil {
|
if signatureError == nil {
|
||||||
signatureError = checkSignatureDetails(key, sig, scr.config)
|
signatureError = checkMessageSignatureDetails(key, sig, scr.config)
|
||||||
}
|
}
|
||||||
scr.md.Signature = sig
|
scr.md.Signature = sig
|
||||||
scr.md.SignatureError = signatureError
|
scr.md.SignatureError = signatureError
|
||||||
|
|
@ -408,16 +435,15 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
|
||||||
// unsigned hash of its own. In order to check this we need to
|
// unsigned hash of its own. In order to check this we need to
|
||||||
// close that Reader.
|
// close that Reader.
|
||||||
if scr.md.decrypted != nil {
|
if scr.md.decrypted != nil {
|
||||||
mdcErr := scr.md.decrypted.Close()
|
if sensitiveParsingError := scr.md.decrypted.Close(); sensitiveParsingError != nil {
|
||||||
if mdcErr != nil {
|
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
|
||||||
return n, mdcErr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return n, io.EOF
|
return n, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
if sensitiveParsingError != nil {
|
if sensitiveParsingError != nil {
|
||||||
return n, errors.StructuralError("parsing error")
|
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, readsDecryptedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
|
|
@ -428,14 +454,13 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
|
||||||
// if any, and a possible signature verification error.
|
// if any, and a possible signature verification error.
|
||||||
// If the signer isn't known, ErrUnknownIssuer is returned.
|
// If the signer isn't known, ErrUnknownIssuer is returned.
|
||||||
func VerifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
|
func VerifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
|
||||||
var expectedHashes []crypto.Hash
|
return verifyDetachedSignature(keyring, signed, signature, nil, false, config)
|
||||||
return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyDetachedSignatureAndHash performs the same actions as
|
// VerifyDetachedSignatureAndHash performs the same actions as
|
||||||
// VerifyDetachedSignature and checks that the expected hash functions were used.
|
// VerifyDetachedSignature and checks that the expected hash functions were used.
|
||||||
func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
|
func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
|
||||||
return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config)
|
return verifyDetachedSignature(keyring, signed, signature, expectedHashes, true, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckDetachedSignature takes a signed file and a detached signature and
|
// CheckDetachedSignature takes a signed file and a detached signature and
|
||||||
|
|
@ -443,25 +468,24 @@ func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader
|
||||||
// signature verification error. If the signer isn't known,
|
// signature verification error. If the signer isn't known,
|
||||||
// ErrUnknownIssuer is returned.
|
// ErrUnknownIssuer is returned.
|
||||||
func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (signer *Entity, err error) {
|
func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (signer *Entity, err error) {
|
||||||
var expectedHashes []crypto.Hash
|
_, signer, err = verifyDetachedSignature(keyring, signed, signature, nil, false, config)
|
||||||
return CheckDetachedSignatureAndHash(keyring, signed, signature, expectedHashes, config)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckDetachedSignatureAndHash performs the same actions as
|
// CheckDetachedSignatureAndHash performs the same actions as
|
||||||
// CheckDetachedSignature and checks that the expected hash functions were used.
|
// CheckDetachedSignature and checks that the expected hash functions were used.
|
||||||
func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (signer *Entity, err error) {
|
func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (signer *Entity, err error) {
|
||||||
_, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, config)
|
_, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, true, config)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
|
func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, checkHashes bool, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
|
||||||
var issuerKeyId uint64
|
var issuerKeyId uint64
|
||||||
var hashFunc crypto.Hash
|
var hashFunc crypto.Hash
|
||||||
var sigType packet.SignatureType
|
var sigType packet.SignatureType
|
||||||
var keys []Key
|
var keys []Key
|
||||||
var p packet.Packet
|
var p packet.Packet
|
||||||
|
|
||||||
expectedHashesLen := len(expectedHashes)
|
|
||||||
packets := packet.NewReader(signature)
|
packets := packet.NewReader(signature)
|
||||||
for {
|
for {
|
||||||
p, err = packets.Next()
|
p, err = packets.Next()
|
||||||
|
|
@ -483,16 +507,19 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec
|
||||||
issuerKeyId = *sig.IssuerKeyId
|
issuerKeyId = *sig.IssuerKeyId
|
||||||
hashFunc = sig.Hash
|
hashFunc = sig.Hash
|
||||||
sigType = sig.SigType
|
sigType = sig.SigType
|
||||||
|
if checkHashes {
|
||||||
for i, expectedHash := range expectedHashes {
|
matchFound := false
|
||||||
if hashFunc == expectedHash {
|
// check for hashes
|
||||||
break
|
for _, expectedHash := range expectedHashes {
|
||||||
|
if hashFunc == expectedHash {
|
||||||
|
matchFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if i+1 == expectedHashesLen {
|
if !matchFound {
|
||||||
return nil, nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers")
|
return nil, nil, errors.StructuralError("hash algorithm or salt mismatch with cleartext message headers")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys = keyring.KeysByIdUsage(issuerKeyId, packet.KeyFlagSign)
|
keys = keyring.KeysByIdUsage(issuerKeyId, packet.KeyFlagSign)
|
||||||
if len(keys) > 0 {
|
if len(keys) > 0 {
|
||||||
break
|
break
|
||||||
|
|
@ -503,7 +530,11 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
h, wrappedHash, err := hashForSignature(hashFunc, sigType)
|
h, err := sig.PrepareVerify()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
wrappedHash, err := wrapHashForSignature(h, sigType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -515,7 +546,7 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
err = key.PublicKey.VerifySignature(h, sig)
|
err = key.PublicKey.VerifySignature(h, sig)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return sig, key.Entity, checkSignatureDetails(&key, sig, config)
|
return sig, key.Entity, checkMessageSignatureDetails(&key, sig, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,7 +564,7 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader,
|
||||||
return CheckDetachedSignature(keyring, signed, body, config)
|
return CheckDetachedSignature(keyring, signed, body, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSignatureDetails returns an error if:
|
// checkMessageSignatureDetails returns an error if:
|
||||||
// - The signature (or one of the binding signatures mentioned below)
|
// - The signature (or one of the binding signatures mentioned below)
|
||||||
// has a unknown critical notation data subpacket
|
// has a unknown critical notation data subpacket
|
||||||
// - The primary key of the signing entity is revoked
|
// - The primary key of the signing entity is revoked
|
||||||
|
|
@ -551,15 +582,11 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader,
|
||||||
// NOTE: The order of these checks is important, as the caller may choose to
|
// NOTE: The order of these checks is important, as the caller may choose to
|
||||||
// ignore ErrSignatureExpired or ErrKeyExpired errors, but should never
|
// ignore ErrSignatureExpired or ErrKeyExpired errors, but should never
|
||||||
// ignore any other errors.
|
// ignore any other errors.
|
||||||
//
|
func checkMessageSignatureDetails(key *Key, signature *packet.Signature, config *packet.Config) error {
|
||||||
// TODO: Also return an error if:
|
|
||||||
// - The primary key is expired according to a direct-key signature
|
|
||||||
// - (For V5 keys only:) The direct-key signature (exists and) is expired
|
|
||||||
func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet.Config) error {
|
|
||||||
now := config.Now()
|
now := config.Now()
|
||||||
primaryIdentity := key.Entity.PrimaryIdentity()
|
primarySelfSignature, primaryIdentity := key.Entity.PrimarySelfSignature()
|
||||||
signedBySubKey := key.PublicKey != key.Entity.PrimaryKey
|
signedBySubKey := key.PublicKey != key.Entity.PrimaryKey
|
||||||
sigsToCheck := []*packet.Signature{signature, primaryIdentity.SelfSignature}
|
sigsToCheck := []*packet.Signature{signature, primarySelfSignature}
|
||||||
if signedBySubKey {
|
if signedBySubKey {
|
||||||
sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature)
|
sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature)
|
||||||
}
|
}
|
||||||
|
|
@ -572,10 +599,10 @@ func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet
|
||||||
}
|
}
|
||||||
if key.Entity.Revoked(now) || // primary key is revoked
|
if key.Entity.Revoked(now) || // primary key is revoked
|
||||||
(signedBySubKey && key.Revoked(now)) || // subkey is revoked
|
(signedBySubKey && key.Revoked(now)) || // subkey is revoked
|
||||||
primaryIdentity.Revoked(now) { // primary identity is revoked
|
(primaryIdentity != nil && primaryIdentity.Revoked(now)) { // primary identity is revoked for v4
|
||||||
return errors.ErrKeyRevoked
|
return errors.ErrKeyRevoked
|
||||||
}
|
}
|
||||||
if key.Entity.PrimaryKey.KeyExpired(primaryIdentity.SelfSignature, now) { // primary key is expired
|
if key.Entity.PrimaryKey.KeyExpired(primarySelfSignature, now) { // primary key is expired
|
||||||
return errors.ErrKeyExpired
|
return errors.ErrKeyExpired
|
||||||
}
|
}
|
||||||
if signedBySubKey {
|
if signedBySubKey {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ const testKeys1And2PrivateHex = "9501d8044d3c5c10010400b1d13382944bd5aba23a43129
|
||||||
|
|
||||||
const dsaElGamalTestKeysHex = "9501e1044dfcb16a110400aa3e5c1a1f43dd28c2ffae8abf5cfce555ee874134d8ba0a0f7b868ce2214beddc74e5e1e21ded354a95d18acdaf69e5e342371a71fbb9093162e0c5f3427de413a7f2c157d83f5cd2f9d791256dc4f6f0e13f13c3302af27f2384075ab3021dff7a050e14854bbde0a1094174855fc02f0bae8e00a340d94a1f22b32e48485700a0cec672ac21258fb95f61de2ce1af74b2c4fa3e6703ff698edc9be22c02ae4d916e4fa223f819d46582c0516235848a77b577ea49018dcd5e9e15cff9dbb4663a1ae6dd7580fa40946d40c05f72814b0f88481207e6c0832c3bded4853ebba0a7e3bd8e8c66df33d5a537cd4acf946d1080e7a3dcea679cb2b11a72a33a2b6a9dc85f466ad2ddf4c3db6283fa645343286971e3dd700703fc0c4e290d45767f370831a90187e74e9972aae5bff488eeff7d620af0362bfb95c1a6c3413ab5d15a2e4139e5d07a54d72583914661ed6a87cce810be28a0aa8879a2dd39e52fb6fe800f4f181ac7e328f740cde3d09a05cecf9483e4cca4253e60d4429ffd679d9996a520012aad119878c941e3cf151459873bdfc2a9563472fe0303027a728f9feb3b864260a1babe83925ce794710cfd642ee4ae0e5b9d74cee49e9c67b6cd0ea5dfbb582132195a121356a1513e1bca73e5b80c58c7ccb4164453412f456c47616d616c2054657374204b65792031886204131102002205024dfcb16a021b03060b090807030206150802090a0b0416020301021e01021780000a091033af447ccd759b09fadd00a0b8fd6f5a790bad7e9f2dbb7632046dc4493588db009c087c6a9ba9f7f49fab221587a74788c00db4889ab00200009d0157044dfcb16a1004008dec3f9291205255ccff8c532318133a6840739dd68b03ba942676f9038612071447bf07d00d559c5c0875724ea16a4c774f80d8338b55fca691a0522e530e604215b467bbc9ccfd483a1da99d7bc2648b4318fdbd27766fc8bfad3fddb37c62b8ae7ccfe9577e9b8d1e77c1d417ed2c2ef02d52f4da11600d85d3229607943700030503ff506c94c87c8cab778e963b76cf63770f0a79bf48fb49d3b4e52234620fc9f7657f9f8d56c96a2b7c7826ae6b57ebb2221a3fe154b03b6637cea7e6d98e3e45d87cf8dc432f723d3d71f89c5192ac8d7290684d2c25ce55846a80c9a7823f6acd9bb29fa6cd71f20bc90eccfca20451d0c976e460e672b000df49466408d527affe0303027a728f9feb3b864260abd761730327bca2aaa4ea0525c175e92bf240682a0e83b226f97ecb2e935b62c9a133858ce31b271fa8eb41f6a1b3cd72a63025ce1a75ee4180dcc284884904181102000905024dfcb16a021b0c000a091033af447ccd759b09dd0b009e3c3e7296092c81bee5a19929462caaf2fff3ae26009e218c437a2340e7ea628149af1ec98ec091a43992b00200009501e1044dfcb1be1104009f61faa61aa43df75d128cbe53de528c4aec49ce9360c992e70c77072ad5623de0a3a6212771b66b39a30dad6781799e92608316900518ec01184a85d872365b7d2ba4bacfb5882ea3c2473d3750dc6178cc1cf82147fb58caa28b28e9f12f6d1efcb0534abed644156c91cca4ab78834268495160b2400bc422beb37d237c2300a0cac94911b6d493bda1e1fbc6feeca7cb7421d34b03fe22cec6ccb39675bb7b94a335c2b7be888fd3906a1125f33301d8aa6ec6ee6878f46f73961c8d57a3e9544d8ef2a2cbfd4d52da665b1266928cfe4cb347a58c412815f3b2d2369dec04b41ac9a71cc9547426d5ab941cccf3b18575637ccfb42df1a802df3cfe0a999f9e7109331170e3a221991bf868543960f8c816c28097e503fe319db10fb98049f3a57d7c80c420da66d56f3644371631fad3f0ff4040a19a4fedc2d07727a1b27576f75a4d28c47d8246f27071e12d7a8de62aad216ddbae6aa02efd6b8a3e2818cda48526549791ab277e447b3a36c57cefe9b592f5eab73959743fcc8e83cbefec03a329b55018b53eec196765ae40ef9e20521a603c551efe0303020950d53a146bf9c66034d00c23130cce95576a2ff78016ca471276e8227fb30b1ffbd92e61804fb0c3eff9e30b1a826ee8f3e4730b4d86273ca977b4164453412f456c47616d616c2054657374204b65792032886204131102002205024dfcb1be021b03060b090807030206150802090a0b0416020301021e01021780000a0910a86bf526325b21b22bd9009e34511620415c974750a20df5cb56b182f3b48e6600a0a9466cb1a1305a84953445f77d461593f1d42bc1b00200009d0157044dfcb1be1004009565a951da1ee87119d600c077198f1c1bceb0f7aa54552489298e41ff788fa8f0d43a69871f0f6f77ebdfb14a4260cf9fbeb65d5844b4272a1904dd95136d06c3da745dc46327dd44a0f16f60135914368c8039a34033862261806bb2c5ce1152e2840254697872c85441ccb7321431d75a747a4bfb1d2c66362b51ce76311700030503fc0ea76601c196768070b7365a200e6ddb09307f262d5f39eec467b5f5784e22abdf1aa49226f59ab37cb49969d8f5230ea65caf56015abda62604544ed526c5c522bf92bed178a078789f6c807b6d34885688024a5bed9e9f8c58d11d4b82487b44c5f470c5606806a0443b79cadb45e0f897a561a53f724e5349b9267c75ca17fe0303020950d53a146bf9c660bc5f4ce8f072465e2d2466434320c1e712272fafc20e342fe7608101580fa1a1a367e60486a7cd1246b7ef5586cf5e10b32762b710a30144f12dd17dd4884904181102000905024dfcb1be021b0c000a0910a86bf526325b21b2904c00a0b2b66b4b39ccffda1d10f3ea8d58f827e30a8b8e009f4255b2d8112a184e40cde43a34e8655ca7809370b0020000"
|
const dsaElGamalTestKeysHex = "9501e1044dfcb16a110400aa3e5c1a1f43dd28c2ffae8abf5cfce555ee874134d8ba0a0f7b868ce2214beddc74e5e1e21ded354a95d18acdaf69e5e342371a71fbb9093162e0c5f3427de413a7f2c157d83f5cd2f9d791256dc4f6f0e13f13c3302af27f2384075ab3021dff7a050e14854bbde0a1094174855fc02f0bae8e00a340d94a1f22b32e48485700a0cec672ac21258fb95f61de2ce1af74b2c4fa3e6703ff698edc9be22c02ae4d916e4fa223f819d46582c0516235848a77b577ea49018dcd5e9e15cff9dbb4663a1ae6dd7580fa40946d40c05f72814b0f88481207e6c0832c3bded4853ebba0a7e3bd8e8c66df33d5a537cd4acf946d1080e7a3dcea679cb2b11a72a33a2b6a9dc85f466ad2ddf4c3db6283fa645343286971e3dd700703fc0c4e290d45767f370831a90187e74e9972aae5bff488eeff7d620af0362bfb95c1a6c3413ab5d15a2e4139e5d07a54d72583914661ed6a87cce810be28a0aa8879a2dd39e52fb6fe800f4f181ac7e328f740cde3d09a05cecf9483e4cca4253e60d4429ffd679d9996a520012aad119878c941e3cf151459873bdfc2a9563472fe0303027a728f9feb3b864260a1babe83925ce794710cfd642ee4ae0e5b9d74cee49e9c67b6cd0ea5dfbb582132195a121356a1513e1bca73e5b80c58c7ccb4164453412f456c47616d616c2054657374204b65792031886204131102002205024dfcb16a021b03060b090807030206150802090a0b0416020301021e01021780000a091033af447ccd759b09fadd00a0b8fd6f5a790bad7e9f2dbb7632046dc4493588db009c087c6a9ba9f7f49fab221587a74788c00db4889ab00200009d0157044dfcb16a1004008dec3f9291205255ccff8c532318133a6840739dd68b03ba942676f9038612071447bf07d00d559c5c0875724ea16a4c774f80d8338b55fca691a0522e530e604215b467bbc9ccfd483a1da99d7bc2648b4318fdbd27766fc8bfad3fddb37c62b8ae7ccfe9577e9b8d1e77c1d417ed2c2ef02d52f4da11600d85d3229607943700030503ff506c94c87c8cab778e963b76cf63770f0a79bf48fb49d3b4e52234620fc9f7657f9f8d56c96a2b7c7826ae6b57ebb2221a3fe154b03b6637cea7e6d98e3e45d87cf8dc432f723d3d71f89c5192ac8d7290684d2c25ce55846a80c9a7823f6acd9bb29fa6cd71f20bc90eccfca20451d0c976e460e672b000df49466408d527affe0303027a728f9feb3b864260abd761730327bca2aaa4ea0525c175e92bf240682a0e83b226f97ecb2e935b62c9a133858ce31b271fa8eb41f6a1b3cd72a63025ce1a75ee4180dcc284884904181102000905024dfcb16a021b0c000a091033af447ccd759b09dd0b009e3c3e7296092c81bee5a19929462caaf2fff3ae26009e218c437a2340e7ea628149af1ec98ec091a43992b00200009501e1044dfcb1be1104009f61faa61aa43df75d128cbe53de528c4aec49ce9360c992e70c77072ad5623de0a3a6212771b66b39a30dad6781799e92608316900518ec01184a85d872365b7d2ba4bacfb5882ea3c2473d3750dc6178cc1cf82147fb58caa28b28e9f12f6d1efcb0534abed644156c91cca4ab78834268495160b2400bc422beb37d237c2300a0cac94911b6d493bda1e1fbc6feeca7cb7421d34b03fe22cec6ccb39675bb7b94a335c2b7be888fd3906a1125f33301d8aa6ec6ee6878f46f73961c8d57a3e9544d8ef2a2cbfd4d52da665b1266928cfe4cb347a58c412815f3b2d2369dec04b41ac9a71cc9547426d5ab941cccf3b18575637ccfb42df1a802df3cfe0a999f9e7109331170e3a221991bf868543960f8c816c28097e503fe319db10fb98049f3a57d7c80c420da66d56f3644371631fad3f0ff4040a19a4fedc2d07727a1b27576f75a4d28c47d8246f27071e12d7a8de62aad216ddbae6aa02efd6b8a3e2818cda48526549791ab277e447b3a36c57cefe9b592f5eab73959743fcc8e83cbefec03a329b55018b53eec196765ae40ef9e20521a603c551efe0303020950d53a146bf9c66034d00c23130cce95576a2ff78016ca471276e8227fb30b1ffbd92e61804fb0c3eff9e30b1a826ee8f3e4730b4d86273ca977b4164453412f456c47616d616c2054657374204b65792032886204131102002205024dfcb1be021b03060b090807030206150802090a0b0416020301021e01021780000a0910a86bf526325b21b22bd9009e34511620415c974750a20df5cb56b182f3b48e6600a0a9466cb1a1305a84953445f77d461593f1d42bc1b00200009d0157044dfcb1be1004009565a951da1ee87119d600c077198f1c1bceb0f7aa54552489298e41ff788fa8f0d43a69871f0f6f77ebdfb14a4260cf9fbeb65d5844b4272a1904dd95136d06c3da745dc46327dd44a0f16f60135914368c8039a34033862261806bb2c5ce1152e2840254697872c85441ccb7321431d75a747a4bfb1d2c66362b51ce76311700030503fc0ea76601c196768070b7365a200e6ddb09307f262d5f39eec467b5f5784e22abdf1aa49226f59ab37cb49969d8f5230ea65caf56015abda62604544ed526c5c522bf92bed178a078789f6c807b6d34885688024a5bed9e9f8c58d11d4b82487b44c5f470c5606806a0443b79cadb45e0f897a561a53f724e5349b9267c75ca17fe0303020950d53a146bf9c660bc5f4ce8f072465e2d2466434320c1e712272fafc20e342fe7608101580fa1a1a367e60486a7cd1246b7ef5586cf5e10b32762b710a30144f12dd17dd4884904181102000905024dfcb1be021b0c000a0910a86bf526325b21b2904c00a0b2b66b4b39ccffda1d10f3ea8d58f827e30a8b8e009f4255b2d8112a184e40cde43a34e8655ca7809370b0020000"
|
||||||
|
|
||||||
|
const ed25519wX25519Key = "c54b0663877fe31b00000020f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3001972817b12be707e8d5f586ce61361201d344eb266a2c82fde6835762b65b0b7c2b1061f1b0a00000042058263877fe3030b090705150a0e080c021600029b03021e09222106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc905270902070200000000ad2820103e2d7d227ec0e6d7ce4471db36bfc97083253690271498a7ef0576c07faae14585b3b903b0127ec4fda2f023045a2ec76bcb4f9571a9651e14aee1137a1d668442c88f951e33c4ffd33fb9a17d511eed758fc6d9cc50cb5fd793b2039d5804c74b0663877fe319000000208693248367f9e5015db922f8f48095dda784987f2d5985b12fbad16caf5e4435004d600a4f794d44775c57a26e0feefed558e9afffd6ad0d582d57fb2ba2dcedb8c29b06181b0a0000002c050263877fe322a106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc9021b0c00000000defa20a6e9186d9d5935fc8fe56314cdb527486a5a5120f9b762a235a729f039010a56b89c658568341fbef3b894e9834ad9bc72afae2f4c9c47a43855e65f1cb0a3f77bbc5f61085c1f8249fe4e7ca59af5f0bcee9398e0fa8d76e522e1d8ab42bb0d"
|
||||||
|
|
||||||
const signedMessageHex = "a3019bc0cbccc0c4b8d8b74ee2108fe16ec6d3ca490cbe362d3f8333d3f352531472538b8b13d353b97232f352158c20943157c71c16064626063656269052062e4e01987e9b6fccff4b7df3a34c534b23e679cbec3bc0f8f6e64dfb4b55fe3f8efa9ce110ddb5cd79faf1d753c51aecfa669f7e7aa043436596cccc3359cb7dd6bbe9ecaa69e5989d9e57209571edc0b2fa7f57b9b79a64ee6e99ce1371395fee92fec2796f7b15a77c386ff668ee27f6d38f0baa6c438b561657377bf6acff3c5947befd7bf4c196252f1d6e5c524d0300"
|
const signedMessageHex = "a3019bc0cbccc0c4b8d8b74ee2108fe16ec6d3ca490cbe362d3f8333d3f352531472538b8b13d353b97232f352158c20943157c71c16064626063656269052062e4e01987e9b6fccff4b7df3a34c534b23e679cbec3bc0f8f6e64dfb4b55fe3f8efa9ce110ddb5cd79faf1d753c51aecfa669f7e7aa043436596cccc3359cb7dd6bbe9ecaa69e5989d9e57209571edc0b2fa7f57b9b79a64ee6e99ce1371395fee92fec2796f7b15a77c386ff668ee27f6d38f0baa6c438b561657377bf6acff3c5947befd7bf4c196252f1d6e5c524d0300"
|
||||||
|
|
||||||
const signedTextMessageHex = "a3019bc0cbccc8c4b8d8b74ee2108fe16ec6d36a250cbece0c178233d3f352531472538b8b13d35379b97232f352158ca0b4312f57c71c1646462606365626906a062e4e019811591798ff99bf8afee860b0d8a8c2a85c3387e3bcf0bb3b17987f2bbcfab2aa526d930cbfd3d98757184df3995c9f3e7790e36e3e9779f06089d4c64e9e47dd6202cb6e9bc73c5d11bb59fbaf89d22d8dc7cf199ddf17af96e77c5f65f9bbed56f427bd8db7af37f6c9984bf9385efaf5f184f986fb3e6adb0ecfe35bbf92d16a7aa2a344fb0bc52fb7624f0200"
|
const signedTextMessageHex = "a3019bc0cbccc8c4b8d8b74ee2108fe16ec6d36a250cbece0c178233d3f352531472538b8b13d35379b97232f352158ca0b4312f57c71c1646462606365626906a062e4e019811591798ff99bf8afee860b0d8a8c2a85c3387e3bcf0bb3b17987f2bbcfab2aa526d930cbfd3d98757184df3995c9f3e7790e36e3e9779f06089d4c64e9e47dd6202cb6e9bc73c5d11bb59fbaf89d22d8dc7cf199ddf17af96e77c5f65f9bbed56f427bd8db7af37f6c9984bf9385efaf5f184f986fb3e6adb0ecfe35bbf92d16a7aa2a344fb0bc52fb7624f0200"
|
||||||
|
|
@ -160,18 +162,78 @@ TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw==
|
||||||
=IiS2
|
=IiS2
|
||||||
-----END PGP PRIVATE KEY BLOCK-----`
|
-----END PGP PRIVATE KEY BLOCK-----`
|
||||||
|
|
||||||
// Generated with the above private key
|
// See OpenPGP crypto refresh Section A.3.
|
||||||
const v5PrivKeyMsg = `-----BEGIN PGP MESSAGE-----
|
const v6PrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
Version: OpenPGP.js v4.10.7
|
|
||||||
Comment: https://openpgpjs.org
|
|
||||||
|
|
||||||
xA0DAQoWGTR7yYckZAIByxF1B21zZy50eHRfbIGSdGVzdMJ3BQEWCgAGBQJf
|
xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB
|
||||||
bIGSACMiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVDQvAP9G
|
exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ
|
||||||
y29VPonFXqi2zKkpZrvyvZxg+n5e8Nt9wNbuxeCd3QD/TtO2s+JvjrE4Siwv
|
BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
|
||||||
UQdl5MlBka1QSNbMq2Bz7XwNPg4=
|
2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh
|
||||||
=6lbM
|
RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe
|
||||||
|
7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/
|
||||||
|
LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG
|
||||||
|
GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
|
||||||
|
2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE
|
||||||
|
M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr
|
||||||
|
k0mXubZvyl4GBg==
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----`
|
||||||
|
|
||||||
|
// See OpenPGP crypto refresh merge request:
|
||||||
|
// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/304
|
||||||
|
const v6PrivKeyMsg = `-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO
|
||||||
|
WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS
|
||||||
|
aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l
|
||||||
|
yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo
|
||||||
|
bhF30A+IitsxxA==
|
||||||
-----END PGP MESSAGE-----`
|
-----END PGP MESSAGE-----`
|
||||||
|
|
||||||
|
// See OpenPGP crypto refresh merge request:
|
||||||
|
// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/305
|
||||||
|
const v6PrivKeyInlineSignMsg = `-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO
|
||||||
|
WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS
|
||||||
|
aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l
|
||||||
|
yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo
|
||||||
|
bhF30A+IitsxxA==
|
||||||
|
-----END PGP MESSAGE-----`
|
||||||
|
|
||||||
|
// See https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/274
|
||||||
|
// decryption password: "correct horse battery staple"
|
||||||
|
const v6ArgonSealedPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC
|
||||||
|
FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS
|
||||||
|
3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC
|
||||||
|
Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW
|
||||||
|
cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin
|
||||||
|
7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/
|
||||||
|
0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0
|
||||||
|
gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf
|
||||||
|
9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR
|
||||||
|
v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr
|
||||||
|
DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki
|
||||||
|
Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt
|
||||||
|
ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----`
|
||||||
|
|
||||||
|
const v4Key25519 = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
xUkEZB3qzRto01j2k2pwN5ux9w70stPinAdXULLr20CRW7U7h2GSeACch0M+
|
||||||
|
qzQg8yjFQ8VBvu3uwgKH9senoHmj72lLSCLTmhFKzQR0ZXN0wogEEBsIAD4F
|
||||||
|
gmQd6s0ECwkHCAmQIf45+TuC+xMDFQgKBBYAAgECGQECmwMCHgEWIQSWEzMi
|
||||||
|
jJUHvyIbVKIh/jn5O4L7EwAAUhaHNlgudvxARdPPETUzVgjuWi+YIz8w1xIb
|
||||||
|
lHQMvIrbe2sGCQIethpWofd0x7DHuv/ciHg+EoxJ/Td6h4pWtIoKx0kEZB3q
|
||||||
|
zRm4CyA7quliq7yx08AoOqHTuuCgvpkSdEhpp3pEyejQOgBo0p6ywIiLPllY
|
||||||
|
0t+jpNspHpAGfXID6oqjpYuJw3AfVRBlwnQEGBsIACoFgmQd6s0JkCH+Ofk7
|
||||||
|
gvsTApsMFiEElhMzIoyVB78iG1SiIf45+TuC+xMAAGgQuN9G73446ykvJ/mL
|
||||||
|
sCZ7zGFId2gBd1EnG0FTC4npfOKpck0X8dngByrCxU8LDSfvjsEp/xDAiKsQ
|
||||||
|
aU71tdtNBQ==
|
||||||
|
=e7jT
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----`
|
||||||
|
|
||||||
const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv
|
xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv
|
||||||
|
|
@ -272,3 +334,124 @@ AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY
|
||||||
hz3tYjKhoFTKEIq3y3Pp
|
hz3tYjKhoFTKEIq3y3Pp
|
||||||
=h/aX
|
=h/aX
|
||||||
-----END PGP PUBLIC KEY BLOCK-----`
|
-----END PGP PUBLIC KEY BLOCK-----`
|
||||||
|
|
||||||
|
const keyv5Test = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
Comment: Bob's OpenPGP Transferable Secret Key
|
||||||
|
|
||||||
|
lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv
|
||||||
|
/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz
|
||||||
|
/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/
|
||||||
|
5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3
|
||||||
|
X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv
|
||||||
|
9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0
|
||||||
|
qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb
|
||||||
|
SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb
|
||||||
|
vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM
|
||||||
|
cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK
|
||||||
|
3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z
|
||||||
|
Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs
|
||||||
|
hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ
|
||||||
|
bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4
|
||||||
|
i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI
|
||||||
|
1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP
|
||||||
|
fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6
|
||||||
|
fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E
|
||||||
|
LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx
|
||||||
|
+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL
|
||||||
|
hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN
|
||||||
|
WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/
|
||||||
|
MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC
|
||||||
|
mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC
|
||||||
|
YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E
|
||||||
|
he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8
|
||||||
|
zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P
|
||||||
|
NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT
|
||||||
|
t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w
|
||||||
|
ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC
|
||||||
|
F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U
|
||||||
|
2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX
|
||||||
|
yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe
|
||||||
|
doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3
|
||||||
|
BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl
|
||||||
|
sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN
|
||||||
|
4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+
|
||||||
|
L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG
|
||||||
|
ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad
|
||||||
|
BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD
|
||||||
|
bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar
|
||||||
|
29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2
|
||||||
|
WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB
|
||||||
|
leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te
|
||||||
|
g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj
|
||||||
|
Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn
|
||||||
|
JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx
|
||||||
|
IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp
|
||||||
|
SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h
|
||||||
|
OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np
|
||||||
|
Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c
|
||||||
|
+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0
|
||||||
|
tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o
|
||||||
|
BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny
|
||||||
|
zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK
|
||||||
|
clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl
|
||||||
|
zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr
|
||||||
|
gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ
|
||||||
|
aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5
|
||||||
|
fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/
|
||||||
|
ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5
|
||||||
|
HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf
|
||||||
|
SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd
|
||||||
|
5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ
|
||||||
|
E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM
|
||||||
|
GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY
|
||||||
|
vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ
|
||||||
|
26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP
|
||||||
|
eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX
|
||||||
|
c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief
|
||||||
|
rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0
|
||||||
|
JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg
|
||||||
|
71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH
|
||||||
|
s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd
|
||||||
|
NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91
|
||||||
|
6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7
|
||||||
|
xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=
|
||||||
|
=miES
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const certv5Test = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
lGEFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd
|
||||||
|
fj75iux+my8QAAAAAAAiAQCHZ1SnSUmWqxEsoI6facIVZQu6mph3cBFzzTvcm5lA
|
||||||
|
Ng5ctBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0e8mHJGQC
|
||||||
|
X5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyICAQYVCgkI
|
||||||
|
CwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3wwJAXRJy9
|
||||||
|
M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2BZxmBVyR9OQSAAAA
|
||||||
|
MgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0YvYWWAoD
|
||||||
|
AQgHAAAAAAAiAP9OdAPppjU1WwpqjIItkxr+VPQRT8Zm/Riw7U3F6v3OiBFHiHoF
|
||||||
|
GBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVAUCXJH05AIb
|
||||||
|
DAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijhob2U5AQC+RtOHCHx7
|
||||||
|
TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw==
|
||||||
|
=IiS2
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const msgv5Test = `-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
wcDMA3wvqk35PDeyAQv+PcQiLsoYTH30nJYQh3j3cJaO2+jErtVCrIQRIU0+
|
||||||
|
rmgMddERYST4A9mA0DQIiTI4FQ0Lp440D3BWCgpq3LlNWewGzduaWwym5rN6
|
||||||
|
cwHz5ccDqOcqbd9X0GXXGy/ZH/ljSgzuVMIytMAXKdF/vrRrVgH/+I7cxvm9
|
||||||
|
HwnhjMN5dF0j4aEt996H2T7cbtzSr2GN9SWGW8Gyu7I8Zx73hgrGUI7gDiJB
|
||||||
|
Afaff+P6hfkkHSGOItr94dde8J/7AUF4VEwwxdVVPvsNEFyvv6gRIbYtOCa2
|
||||||
|
6RE6h1V/QTxW2O7zZgzWALrE2ui0oaYr9QuqQSssd9CdgExLfdPbI+3/ZAnE
|
||||||
|
v31Idzpk3/6ILiakYHtXkElPXvf46mCNpobty8ysT34irF+fy3C1p3oGwAsx
|
||||||
|
5VDV9OSFU6z5U+UPbSPYAy9rkc5ZssuIKxCER2oTvZ2L8Q5cfUvEUiJtRGGn
|
||||||
|
CJlHrVDdp3FssKv2tlKgLkvxJLyoOjuEkj44H1qRk+D02FzmmUT/0sAHAYYx
|
||||||
|
lTir6mjHeLpcGjn4waUuWIAJyph8SxUexP60bic0L0NBa6Qp5SxxijKsPIDb
|
||||||
|
FPHxWwfJSDZRrgUyYT7089YFB/ZM4FHyH9TZcnxn0f0xIB7NS6YNDsxzN2zT
|
||||||
|
EVEYf+De4qT/dQTsdww78Chtcv9JY9r2kDm77dk2MUGHL2j7n8jasbLtgA7h
|
||||||
|
pn2DMIWLrGamMLWRmlwslolKr1sMV5x8w+5Ias6C33iBMl9phkg42an0gYmc
|
||||||
|
byVJHvLO/XErtC+GNIJeMg==
|
||||||
|
=liRq
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
|
`
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,10 @@ func decodeCount(c uint8) int {
|
||||||
// encodeMemory converts the Argon2 "memory" in the range parallelism*8 to
|
// encodeMemory converts the Argon2 "memory" in the range parallelism*8 to
|
||||||
// 2**31, inclusive, to an encoded memory. The return value is the
|
// 2**31, inclusive, to an encoded memory. The return value is the
|
||||||
// octet that is actually stored in the GPG file. encodeMemory panics
|
// octet that is actually stored in the GPG file. encodeMemory panics
|
||||||
// if is not in the above range
|
// if is not in the above range
|
||||||
// See OpenPGP crypto refresh Section 3.7.1.4.
|
// See OpenPGP crypto refresh Section 3.7.1.4.
|
||||||
func encodeMemory(memory uint32, parallelism uint8) uint8 {
|
func encodeMemory(memory uint32, parallelism uint8) uint8 {
|
||||||
if memory < (8 * uint32(parallelism)) || memory > uint32(2147483648) {
|
if memory < (8*uint32(parallelism)) || memory > uint32(2147483648) {
|
||||||
panic("Memory argument memory is outside the required range")
|
panic("Memory argument memory is outside the required range")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,8 +199,8 @@ func Generate(rand io.Reader, c *Config) (*Params, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
params = &Params{
|
params = &Params{
|
||||||
mode: SaltedS2K,
|
mode: SaltedS2K,
|
||||||
hashId: hashId,
|
hashId: hashId,
|
||||||
}
|
}
|
||||||
} else { // Enforce IteratedSaltedS2K method otherwise
|
} else { // Enforce IteratedSaltedS2K method otherwise
|
||||||
hashId, ok := algorithm.HashToHashId(c.hash())
|
hashId, ok := algorithm.HashToHashId(c.hash())
|
||||||
|
|
@ -211,7 +211,7 @@ func Generate(rand io.Reader, c *Config) (*Params, error) {
|
||||||
c.S2KMode = IteratedSaltedS2K
|
c.S2KMode = IteratedSaltedS2K
|
||||||
}
|
}
|
||||||
params = &Params{
|
params = &Params{
|
||||||
mode: IteratedSaltedS2K,
|
mode: IteratedSaltedS2K,
|
||||||
hashId: hashId,
|
hashId: hashId,
|
||||||
countByte: c.EncodedCount(),
|
countByte: c.EncodedCount(),
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +283,9 @@ func ParseIntoParams(r io.Reader) (params *Params, err error) {
|
||||||
params.passes = buf[Argon2SaltSize]
|
params.passes = buf[Argon2SaltSize]
|
||||||
params.parallelism = buf[Argon2SaltSize+1]
|
params.parallelism = buf[Argon2SaltSize+1]
|
||||||
params.memoryExp = buf[Argon2SaltSize+2]
|
params.memoryExp = buf[Argon2SaltSize+2]
|
||||||
|
if err := validateArgon2Params(params); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return params, nil
|
return params, nil
|
||||||
case GnuS2K:
|
case GnuS2K:
|
||||||
// This is a GNU extension. See
|
// This is a GNU extension. See
|
||||||
|
|
@ -300,15 +303,22 @@ func ParseIntoParams(r io.Reader) (params *Params, err error) {
|
||||||
return nil, errors.UnsupportedError("S2K function")
|
return nil, errors.UnsupportedError("S2K function")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (params *Params) Mode() Mode {
|
||||||
|
return params.mode
|
||||||
|
}
|
||||||
|
|
||||||
func (params *Params) Dummy() bool {
|
func (params *Params) Dummy() bool {
|
||||||
return params != nil && params.mode == GnuS2K
|
return params != nil && params.mode == GnuS2K
|
||||||
}
|
}
|
||||||
|
|
||||||
func (params *Params) salt() []byte {
|
func (params *Params) salt() []byte {
|
||||||
switch params.mode {
|
switch params.mode {
|
||||||
case SaltedS2K, IteratedSaltedS2K: return params.saltBytes[:8]
|
case SaltedS2K, IteratedSaltedS2K:
|
||||||
case Argon2S2K: return params.saltBytes[:Argon2SaltSize]
|
return params.saltBytes[:8]
|
||||||
default: return nil
|
case Argon2S2K:
|
||||||
|
return params.saltBytes[:Argon2SaltSize]
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,3 +415,22 @@ func Serialize(w io.Writer, key []byte, rand io.Reader, passphrase []byte, c *Co
|
||||||
f(key, passphrase)
|
f(key, passphrase)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateArgon2Params checks that the argon2 parameters are valid according to RFC9580.
|
||||||
|
func validateArgon2Params(params *Params) error {
|
||||||
|
// The number of passes t and the degree of parallelism p MUST be non-zero.
|
||||||
|
if params.parallelism == 0 {
|
||||||
|
return errors.StructuralError("invalid argon2 params: parallelism is 0")
|
||||||
|
}
|
||||||
|
if params.passes == 0 {
|
||||||
|
return errors.StructuralError("invalid argon2 params: iterations is 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The encoded memory size MUST be a value from 3+ceil(log2(p)) to 31,
|
||||||
|
// such that the decoded memory size m is a value from 8*p to 2^31.
|
||||||
|
if params.memoryExp > 31 || decodeMemory(params.memoryExp) < 8*uint32(params.parallelism) {
|
||||||
|
return errors.StructuralError("invalid argon2 params: memory is out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ package s2k
|
||||||
// the same parameters.
|
// the same parameters.
|
||||||
type Cache map[Params][]byte
|
type Cache map[Params][]byte
|
||||||
|
|
||||||
// GetOrComputeDerivedKey tries to retrieve the key
|
// GetOrComputeDerivedKey tries to retrieve the key
|
||||||
// for the given s2k parameters from the cache.
|
// for the given s2k parameters from the cache.
|
||||||
// If there is no hit, it derives the key with the s2k function from the passphrase,
|
// If there is no hit, it derives the key with the s2k function from the passphrase,
|
||||||
// updates the cache, and returns the key.
|
// updates the cache, and returns the key.
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ type Config struct {
|
||||||
type Argon2Config struct {
|
type Argon2Config struct {
|
||||||
NumberOfPasses uint8
|
NumberOfPasses uint8
|
||||||
DegreeOfParallelism uint8
|
DegreeOfParallelism uint8
|
||||||
// The memory parameter for Argon2 specifies desired memory usage in kibibytes.
|
// Memory specifies the desired Argon2 memory usage in kibibytes.
|
||||||
// For example memory=64*1024 sets the memory cost to ~64 MB.
|
// For example memory=64*1024 sets the memory cost to ~64 MB.
|
||||||
Memory uint32
|
Memory uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Mode() Mode {
|
func (c *Config) Mode() Mode {
|
||||||
|
|
@ -115,7 +115,7 @@ func (c *Argon2Config) EncodedMemory() uint8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
memory := c.Memory
|
memory := c.Memory
|
||||||
lowerBound := uint32(c.Parallelism())*8
|
lowerBound := uint32(c.Parallelism()) * 8
|
||||||
upperBound := uint32(2147483648)
|
upperBound := uint32(2147483648)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,11 @@ func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.S
|
||||||
|
|
||||||
sig := createSignaturePacket(signingKey.PublicKey, sigType, config)
|
sig := createSignaturePacket(signingKey.PublicKey, sigType, config)
|
||||||
|
|
||||||
h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType)
|
h, err := sig.PrepareSign(config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wrappedHash, err := wrapHashForSignature(h, sig.SigType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -275,14 +279,28 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit
|
||||||
return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
|
return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var salt []byte
|
||||||
if signer != nil {
|
if signer != nil {
|
||||||
|
var opsVersion = 3
|
||||||
|
if signer.Version == 6 {
|
||||||
|
opsVersion = signer.Version
|
||||||
|
}
|
||||||
ops := &packet.OnePassSignature{
|
ops := &packet.OnePassSignature{
|
||||||
|
Version: opsVersion,
|
||||||
SigType: sigType,
|
SigType: sigType,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
PubKeyAlgo: signer.PubKeyAlgo,
|
PubKeyAlgo: signer.PubKeyAlgo,
|
||||||
KeyId: signer.KeyId,
|
KeyId: signer.KeyId,
|
||||||
IsLast: true,
|
IsLast: true,
|
||||||
}
|
}
|
||||||
|
if opsVersion == 6 {
|
||||||
|
ops.KeyFingerprint = signer.Fingerprint
|
||||||
|
salt, err = packet.SignatureSaltForHash(hash, config.Random())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ops.Salt = salt
|
||||||
|
}
|
||||||
if err := ops.Serialize(payload); err != nil {
|
if err := ops.Serialize(payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -310,19 +328,19 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer != nil {
|
if signer != nil {
|
||||||
h, wrappedHash, err := hashForSignature(hash, sigType)
|
h, wrappedHash, err := hashForSignature(hash, sigType, salt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
metadata := &packet.LiteralData{
|
metadata := &packet.LiteralData{
|
||||||
Format: 't',
|
Format: 'u',
|
||||||
FileName: hints.FileName,
|
FileName: hints.FileName,
|
||||||
Time: epochSeconds,
|
Time: epochSeconds,
|
||||||
}
|
}
|
||||||
if hints.IsBinary {
|
if hints.IsBinary {
|
||||||
metadata.Format = 'b'
|
metadata.Format = 'b'
|
||||||
}
|
}
|
||||||
return signatureWriter{payload, literalData, hash, wrappedHash, h, signer, sigType, config, metadata}, nil
|
return signatureWriter{payload, literalData, hash, wrappedHash, h, salt, signer, sigType, config, metadata}, nil
|
||||||
}
|
}
|
||||||
return literalData, nil
|
return literalData, nil
|
||||||
}
|
}
|
||||||
|
|
@ -380,15 +398,19 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En
|
||||||
return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys")
|
return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := to[i].PrimaryIdentity().SelfSignature
|
primarySelfSignature, _ := to[i].PrimarySelfSignature()
|
||||||
if !sig.SEIPDv2 {
|
if primarySelfSignature == nil {
|
||||||
|
return nil, errors.InvalidArgumentError("entity without a self-signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !primarySelfSignature.SEIPDv2 {
|
||||||
aeadSupported = false
|
aeadSupported = false
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateCiphers = intersectPreferences(candidateCiphers, sig.PreferredSymmetric)
|
candidateCiphers = intersectPreferences(candidateCiphers, primarySelfSignature.PreferredSymmetric)
|
||||||
candidateHashes = intersectPreferences(candidateHashes, sig.PreferredHash)
|
candidateHashes = intersectPreferences(candidateHashes, primarySelfSignature.PreferredHash)
|
||||||
candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, sig.PreferredCipherSuites)
|
candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, primarySelfSignature.PreferredCipherSuites)
|
||||||
candidateCompression = intersectPreferences(candidateCompression, sig.PreferredCompression)
|
candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the event that the intersection of supported algorithms is empty we use the ones
|
// In the event that the intersection of supported algorithms is empty we use the ones
|
||||||
|
|
@ -422,13 +444,19 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
symKey := make([]byte, cipher.KeySize())
|
var symKey []byte
|
||||||
|
if aeadSupported {
|
||||||
|
symKey = make([]byte, aeadCipherSuite.Cipher.KeySize())
|
||||||
|
} else {
|
||||||
|
symKey = make([]byte, cipher.KeySize())
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := io.ReadFull(config.Random(), symKey); err != nil {
|
if _, err := io.ReadFull(config.Random(), symKey); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range encryptKeys {
|
for _, key := range encryptKeys {
|
||||||
if err := packet.SerializeEncryptedKey(keyWriter, key.PublicKey, cipher, symKey, config); err != nil {
|
if err := packet.SerializeEncryptedKeyAEAD(keyWriter, key.PublicKey, cipher, aeadSupported, symKey, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -465,13 +493,17 @@ func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Con
|
||||||
hashToHashId(crypto.SHA3_512),
|
hashToHashId(crypto.SHA3_512),
|
||||||
}
|
}
|
||||||
defaultHashes := candidateHashes[0:1]
|
defaultHashes := candidateHashes[0:1]
|
||||||
preferredHashes := signed.PrimaryIdentity().SelfSignature.PreferredHash
|
primarySelfSignature, _ := signed.PrimarySelfSignature()
|
||||||
|
if primarySelfSignature == nil {
|
||||||
|
return nil, errors.StructuralError("signed entity has no self-signature")
|
||||||
|
}
|
||||||
|
preferredHashes := primarySelfSignature.PreferredHash
|
||||||
if len(preferredHashes) == 0 {
|
if len(preferredHashes) == 0 {
|
||||||
preferredHashes = defaultHashes
|
preferredHashes = defaultHashes
|
||||||
}
|
}
|
||||||
candidateHashes = intersectPreferences(candidateHashes, preferredHashes)
|
candidateHashes = intersectPreferences(candidateHashes, preferredHashes)
|
||||||
if len(candidateHashes) == 0 {
|
if len(candidateHashes) == 0 {
|
||||||
return nil, errors.InvalidArgumentError("cannot sign because signing key shares no common algorithms with candidate hashes")
|
return nil, errors.StructuralError("cannot sign because signing key shares no common algorithms with candidate hashes")
|
||||||
}
|
}
|
||||||
|
|
||||||
return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, packet.SigTypeBinary, config)
|
return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, packet.SigTypeBinary, config)
|
||||||
|
|
@ -486,6 +518,7 @@ type signatureWriter struct {
|
||||||
hashType crypto.Hash
|
hashType crypto.Hash
|
||||||
wrappedHash hash.Hash
|
wrappedHash hash.Hash
|
||||||
h hash.Hash
|
h hash.Hash
|
||||||
|
salt []byte // v6 only
|
||||||
signer *packet.PrivateKey
|
signer *packet.PrivateKey
|
||||||
sigType packet.SignatureType
|
sigType packet.SignatureType
|
||||||
config *packet.Config
|
config *packet.Config
|
||||||
|
|
@ -509,6 +542,10 @@ func (s signatureWriter) Close() error {
|
||||||
sig.Hash = s.hashType
|
sig.Hash = s.hashType
|
||||||
sig.Metadata = s.metadata
|
sig.Metadata = s.metadata
|
||||||
|
|
||||||
|
if err := sig.SetSalt(s.salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := sig.Sign(s.h, s.signer, s.config); err != nil {
|
if err := sig.Sign(s.h, s.signer, s.config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
package x25519
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/aes/keywrap"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
x25519lib "github.com/cloudflare/circl/dh/x25519"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hkdfInfo = "OpenPGP X25519"
|
||||||
|
aes128KeySize = 16
|
||||||
|
// The size of a public or private key in bytes.
|
||||||
|
KeySize = x25519lib.Size
|
||||||
|
)
|
||||||
|
|
||||||
|
type PublicKey struct {
|
||||||
|
// Point represents the encoded elliptic curve point of the public key.
|
||||||
|
Point []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrivateKey struct {
|
||||||
|
PublicKey
|
||||||
|
// Secret represents the secret of the private key.
|
||||||
|
Secret []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey creates a new empty private key including the public key.
|
||||||
|
func NewPrivateKey(key PublicKey) *PrivateKey {
|
||||||
|
return &PrivateKey{
|
||||||
|
PublicKey: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates that the provided public key matches the private key.
|
||||||
|
func Validate(pk *PrivateKey) (err error) {
|
||||||
|
var expectedPublicKey, privateKey x25519lib.Key
|
||||||
|
subtle.ConstantTimeCopy(1, privateKey[:], pk.Secret)
|
||||||
|
x25519lib.KeyGen(&expectedPublicKey, &privateKey)
|
||||||
|
if subtle.ConstantTimeCompare(expectedPublicKey[:], pk.PublicKey.Point) == 0 {
|
||||||
|
return errors.KeyInvalidError("x25519: invalid key")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey generates a new x25519 key pair.
|
||||||
|
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
|
||||||
|
var privateKey, publicKey x25519lib.Key
|
||||||
|
privateKeyOut := new(PrivateKey)
|
||||||
|
err := generateKey(rand, &privateKey, &publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateKeyOut.PublicKey.Point = publicKey[:]
|
||||||
|
privateKeyOut.Secret = privateKey[:]
|
||||||
|
return privateKeyOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey(rand io.Reader, privateKey *x25519lib.Key, publicKey *x25519lib.Key) error {
|
||||||
|
maxRounds := 10
|
||||||
|
isZero := true
|
||||||
|
for round := 0; isZero; round++ {
|
||||||
|
if round == maxRounds {
|
||||||
|
return errors.InvalidArgumentError("x25519: zero keys only, randomness source might be corrupt")
|
||||||
|
}
|
||||||
|
_, err := io.ReadFull(rand, privateKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
isZero = constantTimeIsZero(privateKey[:])
|
||||||
|
}
|
||||||
|
x25519lib.KeyGen(publicKey, privateKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts a sessionKey with x25519 according to
|
||||||
|
// the OpenPGP crypto refresh specification section 5.1.6. The function assumes that the
|
||||||
|
// sessionKey has the correct format and padding according to the specification.
|
||||||
|
func Encrypt(rand io.Reader, publicKey *PublicKey, sessionKey []byte) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, err error) {
|
||||||
|
var ephemeralPrivate, ephemeralPublic, staticPublic, shared x25519lib.Key
|
||||||
|
// Check that the input static public key has 32 bytes
|
||||||
|
if len(publicKey.Point) != KeySize {
|
||||||
|
err = errors.KeyInvalidError("x25519: the public key has the wrong size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(staticPublic[:], publicKey.Point)
|
||||||
|
// Generate ephemeral keyPair
|
||||||
|
err = generateKey(rand, &ephemeralPrivate, &ephemeralPublic)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Compute shared key
|
||||||
|
ok := x25519lib.Shared(&shared, &ephemeralPrivate, &staticPublic)
|
||||||
|
if !ok {
|
||||||
|
err = errors.KeyInvalidError("x25519: the public key is a low order point")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Derive the encryption key from the shared secret
|
||||||
|
encryptionKey := applyHKDF(ephemeralPublic[:], publicKey.Point[:], shared[:])
|
||||||
|
ephemeralPublicKey = &PublicKey{
|
||||||
|
Point: ephemeralPublic[:],
|
||||||
|
}
|
||||||
|
// Encrypt the sessionKey with aes key wrapping
|
||||||
|
encryptedSessionKey, err = keywrap.Wrap(encryptionKey, sessionKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts a session key stored in ciphertext with the provided x25519
|
||||||
|
// private key and ephemeral public key.
|
||||||
|
func Decrypt(privateKey *PrivateKey, ephemeralPublicKey *PublicKey, ciphertext []byte) (encodedSessionKey []byte, err error) {
|
||||||
|
var ephemeralPublic, staticPrivate, shared x25519lib.Key
|
||||||
|
// Check that the input ephemeral public key has 32 bytes
|
||||||
|
if len(ephemeralPublicKey.Point) != KeySize {
|
||||||
|
err = errors.KeyInvalidError("x25519: the public key has the wrong size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(ephemeralPublic[:], ephemeralPublicKey.Point)
|
||||||
|
subtle.ConstantTimeCopy(1, staticPrivate[:], privateKey.Secret)
|
||||||
|
// Compute shared key
|
||||||
|
ok := x25519lib.Shared(&shared, &staticPrivate, &ephemeralPublic)
|
||||||
|
if !ok {
|
||||||
|
err = errors.KeyInvalidError("x25519: the ephemeral public key is a low order point")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Derive the encryption key from the shared secret
|
||||||
|
encryptionKey := applyHKDF(ephemeralPublicKey.Point[:], privateKey.PublicKey.Point[:], shared[:])
|
||||||
|
// Decrypt the session key with aes key wrapping
|
||||||
|
encodedSessionKey, err = keywrap.Unwrap(encryptionKey, ciphertext)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyHKDF(ephemeralPublicKey []byte, publicKey []byte, sharedSecret []byte) []byte {
|
||||||
|
inputKey := make([]byte, 3*KeySize)
|
||||||
|
// ephemeral public key | recipient public key | shared secret
|
||||||
|
subtle.ConstantTimeCopy(1, inputKey[:KeySize], ephemeralPublicKey)
|
||||||
|
subtle.ConstantTimeCopy(1, inputKey[KeySize:2*KeySize], publicKey)
|
||||||
|
subtle.ConstantTimeCopy(1, inputKey[2*KeySize:], sharedSecret)
|
||||||
|
hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, []byte(hkdfInfo))
|
||||||
|
encryptionKey := make([]byte, aes128KeySize)
|
||||||
|
_, _ = io.ReadFull(hkdfReader, encryptionKey)
|
||||||
|
return encryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func constantTimeIsZero(bytes []byte) bool {
|
||||||
|
isZero := byte(0)
|
||||||
|
for _, b := range bytes {
|
||||||
|
isZero |= b
|
||||||
|
}
|
||||||
|
return isZero == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENCODING/DECODING ciphertexts:
|
||||||
|
|
||||||
|
// EncodeFieldsLength returns the length of the ciphertext encoding
|
||||||
|
// given the encrypted session key.
|
||||||
|
func EncodedFieldsLength(encryptedSessionKey []byte, v6 bool) int {
|
||||||
|
lenCipherFunction := 0
|
||||||
|
if !v6 {
|
||||||
|
lenCipherFunction = 1
|
||||||
|
}
|
||||||
|
return KeySize + 1 + len(encryptedSessionKey) + lenCipherFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeField encodes x25519 session key encryption fields as
|
||||||
|
// ephemeral x25519 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey
|
||||||
|
// and writes it to writer.
|
||||||
|
func EncodeFields(writer io.Writer, ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) {
|
||||||
|
lenAlgorithm := 0
|
||||||
|
if !v6 {
|
||||||
|
lenAlgorithm = 1
|
||||||
|
}
|
||||||
|
if _, err = writer.Write(ephemeralPublicKey.Point); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = writer.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !v6 {
|
||||||
|
if _, err = writer.Write([]byte{cipherFunction}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = writer.Write(encryptedSessionKey)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeField decodes a x25519 session key encryption as
|
||||||
|
// ephemeral x25519 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey.
|
||||||
|
func DecodeFields(reader io.Reader, v6 bool) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, err error) {
|
||||||
|
var buf [1]byte
|
||||||
|
ephemeralPublicKey = &PublicKey{
|
||||||
|
Point: make([]byte, KeySize),
|
||||||
|
}
|
||||||
|
// 32 octets representing an ephemeral x25519 public key.
|
||||||
|
if _, err = io.ReadFull(reader, ephemeralPublicKey.Point); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
// A one-octet size of the following fields.
|
||||||
|
if _, err = io.ReadFull(reader, buf[:]); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
followingLen := buf[0]
|
||||||
|
// The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet).
|
||||||
|
if !v6 {
|
||||||
|
if _, err = io.ReadFull(reader, buf[:]); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
cipherFunction = buf[0]
|
||||||
|
followingLen -= 1
|
||||||
|
}
|
||||||
|
// The encrypted session key.
|
||||||
|
encryptedSessionKey = make([]byte, followingLen)
|
||||||
|
if _, err = io.ReadFull(reader, encryptedSessionKey); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
return ephemeralPublicKey, encryptedSessionKey, cipherFunction, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
package x448
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/subtle"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/aes/keywrap"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||||
|
x448lib "github.com/cloudflare/circl/dh/x448"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hkdfInfo = "OpenPGP X448"
|
||||||
|
aes256KeySize = 32
|
||||||
|
// The size of a public or private key in bytes.
|
||||||
|
KeySize = x448lib.Size
|
||||||
|
)
|
||||||
|
|
||||||
|
type PublicKey struct {
|
||||||
|
// Point represents the encoded elliptic curve point of the public key.
|
||||||
|
Point []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrivateKey struct {
|
||||||
|
PublicKey
|
||||||
|
// Secret represents the secret of the private key.
|
||||||
|
Secret []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey creates a new empty private key including the public key.
|
||||||
|
func NewPrivateKey(key PublicKey) *PrivateKey {
|
||||||
|
return &PrivateKey{
|
||||||
|
PublicKey: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates that the provided public key matches
|
||||||
|
// the private key.
|
||||||
|
func Validate(pk *PrivateKey) (err error) {
|
||||||
|
var expectedPublicKey, privateKey x448lib.Key
|
||||||
|
subtle.ConstantTimeCopy(1, privateKey[:], pk.Secret)
|
||||||
|
x448lib.KeyGen(&expectedPublicKey, &privateKey)
|
||||||
|
if subtle.ConstantTimeCompare(expectedPublicKey[:], pk.PublicKey.Point) == 0 {
|
||||||
|
return errors.KeyInvalidError("x448: invalid key")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey generates a new x448 key pair.
|
||||||
|
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
|
||||||
|
var privateKey, publicKey x448lib.Key
|
||||||
|
privateKeyOut := new(PrivateKey)
|
||||||
|
err := generateKey(rand, &privateKey, &publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateKeyOut.PublicKey.Point = publicKey[:]
|
||||||
|
privateKeyOut.Secret = privateKey[:]
|
||||||
|
return privateKeyOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey(rand io.Reader, privateKey *x448lib.Key, publicKey *x448lib.Key) error {
|
||||||
|
maxRounds := 10
|
||||||
|
isZero := true
|
||||||
|
for round := 0; isZero; round++ {
|
||||||
|
if round == maxRounds {
|
||||||
|
return errors.InvalidArgumentError("x448: zero keys only, randomness source might be corrupt")
|
||||||
|
}
|
||||||
|
_, err := io.ReadFull(rand, privateKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
isZero = constantTimeIsZero(privateKey[:])
|
||||||
|
}
|
||||||
|
x448lib.KeyGen(publicKey, privateKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts a sessionKey with x448 according to
|
||||||
|
// the OpenPGP crypto refresh specification section 5.1.7. The function assumes that the
|
||||||
|
// sessionKey has the correct format and padding according to the specification.
|
||||||
|
func Encrypt(rand io.Reader, publicKey *PublicKey, sessionKey []byte) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, err error) {
|
||||||
|
var ephemeralPrivate, ephemeralPublic, staticPublic, shared x448lib.Key
|
||||||
|
// Check that the input static public key has 56 bytes.
|
||||||
|
if len(publicKey.Point) != KeySize {
|
||||||
|
err = errors.KeyInvalidError("x448: the public key has the wrong size")
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
copy(staticPublic[:], publicKey.Point)
|
||||||
|
// Generate ephemeral keyPair.
|
||||||
|
if err = generateKey(rand, &ephemeralPrivate, &ephemeralPublic); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
// Compute shared key.
|
||||||
|
ok := x448lib.Shared(&shared, &ephemeralPrivate, &staticPublic)
|
||||||
|
if !ok {
|
||||||
|
err = errors.KeyInvalidError("x448: the public key is a low order point")
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
// Derive the encryption key from the shared secret.
|
||||||
|
encryptionKey := applyHKDF(ephemeralPublic[:], publicKey.Point[:], shared[:])
|
||||||
|
ephemeralPublicKey = &PublicKey{
|
||||||
|
Point: ephemeralPublic[:],
|
||||||
|
}
|
||||||
|
// Encrypt the sessionKey with aes key wrapping.
|
||||||
|
encryptedSessionKey, err = keywrap.Wrap(encryptionKey, sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return ephemeralPublicKey, encryptedSessionKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts a session key stored in ciphertext with the provided x448
|
||||||
|
// private key and ephemeral public key.
|
||||||
|
func Decrypt(privateKey *PrivateKey, ephemeralPublicKey *PublicKey, ciphertext []byte) (encodedSessionKey []byte, err error) {
|
||||||
|
var ephemeralPublic, staticPrivate, shared x448lib.Key
|
||||||
|
// Check that the input ephemeral public key has 56 bytes.
|
||||||
|
if len(ephemeralPublicKey.Point) != KeySize {
|
||||||
|
err = errors.KeyInvalidError("x448: the public key has the wrong size")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copy(ephemeralPublic[:], ephemeralPublicKey.Point)
|
||||||
|
subtle.ConstantTimeCopy(1, staticPrivate[:], privateKey.Secret)
|
||||||
|
// Compute shared key.
|
||||||
|
ok := x448lib.Shared(&shared, &staticPrivate, &ephemeralPublic)
|
||||||
|
if !ok {
|
||||||
|
err = errors.KeyInvalidError("x448: the ephemeral public key is a low order point")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Derive the encryption key from the shared secret.
|
||||||
|
encryptionKey := applyHKDF(ephemeralPublicKey.Point[:], privateKey.PublicKey.Point[:], shared[:])
|
||||||
|
// Decrypt the session key with aes key wrapping.
|
||||||
|
encodedSessionKey, err = keywrap.Unwrap(encryptionKey, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return encodedSessionKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyHKDF(ephemeralPublicKey []byte, publicKey []byte, sharedSecret []byte) []byte {
|
||||||
|
inputKey := make([]byte, 3*KeySize)
|
||||||
|
// ephemeral public key | recipient public key | shared secret.
|
||||||
|
subtle.ConstantTimeCopy(1, inputKey[:KeySize], ephemeralPublicKey)
|
||||||
|
subtle.ConstantTimeCopy(1, inputKey[KeySize:2*KeySize], publicKey)
|
||||||
|
subtle.ConstantTimeCopy(1, inputKey[2*KeySize:], sharedSecret)
|
||||||
|
hkdfReader := hkdf.New(sha512.New, inputKey, []byte{}, []byte(hkdfInfo))
|
||||||
|
encryptionKey := make([]byte, aes256KeySize)
|
||||||
|
_, _ = io.ReadFull(hkdfReader, encryptionKey)
|
||||||
|
return encryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func constantTimeIsZero(bytes []byte) bool {
|
||||||
|
isZero := byte(0)
|
||||||
|
for _, b := range bytes {
|
||||||
|
isZero |= b
|
||||||
|
}
|
||||||
|
return isZero == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENCODING/DECODING ciphertexts:
|
||||||
|
|
||||||
|
// EncodeFieldsLength returns the length of the ciphertext encoding
|
||||||
|
// given the encrypted session key.
|
||||||
|
func EncodedFieldsLength(encryptedSessionKey []byte, v6 bool) int {
|
||||||
|
lenCipherFunction := 0
|
||||||
|
if !v6 {
|
||||||
|
lenCipherFunction = 1
|
||||||
|
}
|
||||||
|
return KeySize + 1 + len(encryptedSessionKey) + lenCipherFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeField encodes x448 session key encryption fields as
|
||||||
|
// ephemeral x448 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey
|
||||||
|
// and writes it to writer.
|
||||||
|
func EncodeFields(writer io.Writer, ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) {
|
||||||
|
lenAlgorithm := 0
|
||||||
|
if !v6 {
|
||||||
|
lenAlgorithm = 1
|
||||||
|
}
|
||||||
|
if _, err = writer.Write(ephemeralPublicKey.Point); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = writer.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !v6 {
|
||||||
|
if _, err = writer.Write([]byte{cipherFunction}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = writer.Write(encryptedSessionKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeField decodes a x448 session key encryption as
|
||||||
|
// ephemeral x448 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey.
|
||||||
|
func DecodeFields(reader io.Reader, v6 bool) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, err error) {
|
||||||
|
var buf [1]byte
|
||||||
|
ephemeralPublicKey = &PublicKey{
|
||||||
|
Point: make([]byte, KeySize),
|
||||||
|
}
|
||||||
|
// 56 octets representing an ephemeral x448 public key.
|
||||||
|
if _, err = io.ReadFull(reader, ephemeralPublicKey.Point); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
// A one-octet size of the following fields.
|
||||||
|
if _, err = io.ReadFull(reader, buf[:]); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
followingLen := buf[0]
|
||||||
|
// The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet).
|
||||||
|
if !v6 {
|
||||||
|
if _, err = io.ReadFull(reader, buf[:]); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
cipherFunction = buf[0]
|
||||||
|
followingLen -= 1
|
||||||
|
}
|
||||||
|
// The encrypted session key.
|
||||||
|
encryptedSessionKey = make([]byte, followingLen)
|
||||||
|
if _, err = io.ReadFull(reader, encryptedSessionKey); err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
return ephemeralPublicKey, encryptedSessionKey, cipherFunction, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
# Changelog #
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased] ##
|
||||||
|
|
||||||
|
## [0.3.6] - 2024-12-17 ##
|
||||||
|
|
||||||
|
### Compatibility ###
|
||||||
|
- The minimum Go version requirement for `filepath-securejoin` is now Go 1.18
|
||||||
|
(we use generics internally).
|
||||||
|
|
||||||
|
For reference, `filepath-securejoin@v0.3.0` somewhat-arbitrarily bumped the
|
||||||
|
Go version requirement to 1.21.
|
||||||
|
|
||||||
|
While we did make some use of Go 1.21 stdlib features (and in principle Go
|
||||||
|
versions <= 1.21 are no longer even supported by upstream anymore), some
|
||||||
|
downstreams have complained that the version bump has meant that they have to
|
||||||
|
do workarounds when backporting fixes that use the new `filepath-securejoin`
|
||||||
|
API onto old branches. This is not an ideal situation, but since using this
|
||||||
|
library is probably better for most downstreams than a hand-rolled
|
||||||
|
workaround, we now have compatibility shims that allow us to build on older
|
||||||
|
Go versions.
|
||||||
|
- Lower minimum version requirement for `golang.org/x/sys` to `v0.18.0` (we
|
||||||
|
need the wrappers for `fsconfig(2)`), which should also make backporting
|
||||||
|
patches to older branches easier.
|
||||||
|
|
||||||
|
## [0.3.5] - 2024-12-06 ##
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- `MkdirAll` will now no longer return an `EEXIST` error if two racing
|
||||||
|
processes are creating the same directory. We will still verify that the path
|
||||||
|
is a directory, but this will avoid spurious errors when multiple threads or
|
||||||
|
programs are trying to `MkdirAll` the same path. opencontainers/runc#4543
|
||||||
|
|
||||||
|
## [0.3.4] - 2024-10-09 ##
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- Previously, some testing mocks we had resulted in us doing `import "testing"`
|
||||||
|
in non-`_test.go` code, which made some downstreams like Kubernetes unhappy.
|
||||||
|
This has been fixed. (#32)
|
||||||
|
|
||||||
|
## [0.3.3] - 2024-09-30 ##
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- The mode and owner verification logic in `MkdirAll` has been removed. This
|
||||||
|
was originally intended to protect against some theoretical attacks but upon
|
||||||
|
further consideration these protections don't actually buy us anything and
|
||||||
|
they were causing spurious errors with more complicated filesystem setups.
|
||||||
|
- The "is the created directory empty" logic in `MkdirAll` has also been
|
||||||
|
removed. This was not causing us issues yet, but some pseudofilesystems (such
|
||||||
|
as `cgroup`) create non-empty directories and so this logic would've been
|
||||||
|
wrong for such cases.
|
||||||
|
|
||||||
|
## [0.3.2] - 2024-09-13 ##
|
||||||
|
|
||||||
|
### Changed ###
|
||||||
|
- Passing the `S_ISUID` or `S_ISGID` modes to `MkdirAllInRoot` will now return
|
||||||
|
an explicit error saying that those bits are ignored by `mkdirat(2)`. In the
|
||||||
|
past a different error was returned, but since the silent ignoring behaviour
|
||||||
|
is codified in the man pages a more explicit error seems apt. While silently
|
||||||
|
ignoring these bits would be the most compatible option, it could lead to
|
||||||
|
users thinking their code sets these bits when it doesn't. Programs that need
|
||||||
|
to deal with compatibility can mask the bits themselves. (#23, #25)
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- If a directory has `S_ISGID` set, then all child directories will have
|
||||||
|
`S_ISGID` set when created and a different gid will be used for any inode
|
||||||
|
created under the directory. Previously, the "expected owner and mode"
|
||||||
|
validation in `securejoin.MkdirAll` did not correctly handle this. We now
|
||||||
|
correctly handle this case. (#24, #25)
|
||||||
|
|
||||||
|
## [0.3.1] - 2024-07-23 ##
|
||||||
|
|
||||||
|
### Changed ###
|
||||||
|
- By allowing `Open(at)InRoot` to opt-out of the extra work done by `MkdirAll`
|
||||||
|
to do the necessary "partial lookups", `Open(at)InRoot` now does less work
|
||||||
|
for both implementations (resulting in a many-fold decrease in the number of
|
||||||
|
operations for `openat2`, and a modest improvement for non-`openat2`) and is
|
||||||
|
far more guaranteed to match the correct `openat2(RESOLVE_IN_ROOT)`
|
||||||
|
behaviour.
|
||||||
|
- We now use `readlinkat(fd, "")` where possible. For `Open(at)InRoot` this
|
||||||
|
effectively just means that we no longer risk getting spurious errors during
|
||||||
|
rename races. However, for our hardened procfs handler, this in theory should
|
||||||
|
prevent mount attacks from tricking us when doing magic-link readlinks (even
|
||||||
|
when using the unsafe host `/proc` handle). Unfortunately `Reopen` is still
|
||||||
|
potentially vulnerable to those kinds of somewhat-esoteric attacks.
|
||||||
|
|
||||||
|
Technically this [will only work on post-2.6.39 kernels][linux-readlinkat-emptypath]
|
||||||
|
but it seems incredibly unlikely anyone is using `filepath-securejoin` on a
|
||||||
|
pre-2011 kernel.
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- Several improvements were made to the errors returned by `Open(at)InRoot` and
|
||||||
|
`MkdirAll` when dealing with invalid paths under the emulated (ie.
|
||||||
|
non-`openat2`) implementation. Previously, some paths would return the wrong
|
||||||
|
error (`ENOENT` when the last component was a non-directory), and other paths
|
||||||
|
would be returned as though they were acceptable (trailing-slash components
|
||||||
|
after a non-directory would be ignored by `Open(at)InRoot`).
|
||||||
|
|
||||||
|
These changes were done to match `openat2`'s behaviour and purely is a
|
||||||
|
consistency fix (most users are going to be using `openat2` anyway).
|
||||||
|
|
||||||
|
[linux-readlinkat-emptypath]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=65cfc6722361570bfe255698d9cd4dccaf47570d
|
||||||
|
|
||||||
|
## [0.3.0] - 2024-07-11 ##
|
||||||
|
|
||||||
|
### Added ###
|
||||||
|
- A new set of `*os.File`-based APIs have been added. These are adapted from
|
||||||
|
[libpathrs][] and we strongly suggest using them if possible (as they provide
|
||||||
|
far more protection against attacks than `SecureJoin`):
|
||||||
|
|
||||||
|
- `Open(at)InRoot` resolves a path inside a rootfs and returns an `*os.File`
|
||||||
|
handle to the path. Note that the handle returned is an `O_PATH` handle,
|
||||||
|
which cannot be used for reading or writing (as well as some other
|
||||||
|
operations -- [see open(2) for more details][open.2])
|
||||||
|
|
||||||
|
- `Reopen` takes an `O_PATH` file handle and safely re-opens it to upgrade
|
||||||
|
it to a regular handle. This can also be used with non-`O_PATH` handles,
|
||||||
|
but `O_PATH` is the most obvious application.
|
||||||
|
|
||||||
|
- `MkdirAll` is an implementation of `os.MkdirAll` that is safe to use to
|
||||||
|
create a directory tree within a rootfs.
|
||||||
|
|
||||||
|
As these are new APIs, they may change in the future. However, they should be
|
||||||
|
safe to start migrating to as we have extensive tests ensuring they behave
|
||||||
|
correctly and are safe against various races and other attacks.
|
||||||
|
|
||||||
|
[libpathrs]: https://github.com/openSUSE/libpathrs
|
||||||
|
[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
|
||||||
|
|
||||||
|
## [0.2.5] - 2024-05-03 ##
|
||||||
|
|
||||||
|
### Changed ###
|
||||||
|
- Some minor changes were made to how lexical components (like `..` and `.`)
|
||||||
|
are handled during path generation in `SecureJoin`. There is no behaviour
|
||||||
|
change as a result of this fix (the resulting paths are the same).
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- The error returned when we hit a symlink loop now references the correct
|
||||||
|
path. (#10)
|
||||||
|
|
||||||
|
## [0.2.4] - 2023-09-06 ##
|
||||||
|
|
||||||
|
### Security ###
|
||||||
|
- This release fixes a potential security issue in filepath-securejoin when
|
||||||
|
used on Windows ([GHSA-6xv5-86q9-7xr8][], which could be used to generate
|
||||||
|
paths outside of the provided rootfs in certain cases), as well as improving
|
||||||
|
the overall behaviour of filepath-securejoin when dealing with Windows paths
|
||||||
|
that contain volume names. Thanks to Paulo Gomes for discovering and fixing
|
||||||
|
these issues.
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- Switch to GitHub Actions for CI so we can test on Windows as well as Linux
|
||||||
|
and MacOS.
|
||||||
|
|
||||||
|
[GHSA-6xv5-86q9-7xr8]: https://github.com/advisories/GHSA-6xv5-86q9-7xr8
|
||||||
|
|
||||||
|
## [0.2.3] - 2021-06-04 ##
|
||||||
|
|
||||||
|
### Changed ###
|
||||||
|
- Switch to Go 1.13-style `%w` error wrapping, letting us drop the dependency
|
||||||
|
on `github.com/pkg/errors`.
|
||||||
|
|
||||||
|
## [0.2.2] - 2018-09-05 ##
|
||||||
|
|
||||||
|
### Changed ###
|
||||||
|
- Use `syscall.ELOOP` as the base error for symlink loops, rather than our own
|
||||||
|
(internal) error. This allows callers to more easily use `errors.Is` to check
|
||||||
|
for this case.
|
||||||
|
|
||||||
|
## [0.2.1] - 2018-09-05 ##
|
||||||
|
|
||||||
|
### Fixed ###
|
||||||
|
- Use our own `IsNotExist` implementation, which lets us handle `ENOTDIR`
|
||||||
|
properly within `SecureJoin`.
|
||||||
|
|
||||||
|
## [0.2.0] - 2017-07-19 ##
|
||||||
|
|
||||||
|
We now have 100% test coverage!
|
||||||
|
|
||||||
|
### Added ###
|
||||||
|
- Add a `SecureJoinVFS` API that can be used for mocking (as we do in our new
|
||||||
|
tests) or for implementing custom handling of lookup operations (such as for
|
||||||
|
rootless containers, where work is necessary to access directories with weird
|
||||||
|
modes because we don't have `CAP_DAC_READ_SEARCH` or `CAP_DAC_OVERRIDE`).
|
||||||
|
|
||||||
|
## 0.1.0 - 2017-07-19
|
||||||
|
|
||||||
|
This is our first release of `github.com/cyphar/filepath-securejoin`,
|
||||||
|
containing a full implementation with a coverage of 93.5% (the only missing
|
||||||
|
cases are the error cases, which are hard to mocktest at the moment).
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...HEAD
|
||||||
|
[0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6
|
||||||
|
[0.3.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...v0.3.5
|
||||||
|
[0.3.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.3...v0.3.4
|
||||||
|
[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.2...v0.3.3
|
||||||
|
[0.3.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.1...v0.3.2
|
||||||
|
[0.3.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.0...v0.3.1
|
||||||
|
[0.3.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.5...v0.3.0
|
||||||
|
[0.2.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.2.5
|
||||||
|
[0.2.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4
|
||||||
|
[0.2.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.2...v0.2.3
|
||||||
|
[0.2.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.1...v0.2.2
|
||||||
|
[0.2.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.0...v0.2.1
|
||||||
|
[0.2.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.1.0...v0.2.0
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
||||||
Copyright (C) 2017 SUSE LLC. All rights reserved.
|
Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,26 @@
|
||||||
## `filepath-securejoin` ##
|
## `filepath-securejoin` ##
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
|
||||||
[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
|
[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
|
||||||
|
|
||||||
An implementation of `SecureJoin`, a [candidate for inclusion in the Go
|
### Old API ###
|
||||||
standard library][go#20126]. The purpose of this function is to be a "secure"
|
|
||||||
alternative to `filepath.Join`, and in particular it provides certain
|
|
||||||
guarantees that are not provided by `filepath.Join`.
|
|
||||||
|
|
||||||
> **NOTE**: This code is *only* safe if you are not at risk of other processes
|
This library was originally just an implementation of `SecureJoin` which was
|
||||||
> modifying path components after you've used `SecureJoin`. If it is possible
|
[intended to be included in the Go standard library][go#20126] as a safer
|
||||||
> for a malicious process to modify path components of the resolved path, then
|
`filepath.Join` that would restrict the path lookup to be inside a root
|
||||||
> you will be vulnerable to some fairly trivial TOCTOU race conditions. [There
|
directory.
|
||||||
> are some Linux kernel patches I'm working on which might allow for a better
|
|
||||||
> solution.][lwn-obeneath]
|
|
||||||
>
|
|
||||||
> In addition, with a slightly modified API it might be possible to use
|
|
||||||
> `O_PATH` and verify that the opened path is actually the resolved one -- but
|
|
||||||
> I have not done that yet. I might add it in the future as a helper function
|
|
||||||
> to help users verify the path (we can't just return `/proc/self/fd/<foo>`
|
|
||||||
> because that doesn't always work transparently for all users).
|
|
||||||
|
|
||||||
This is the function prototype:
|
The implementation was based on code that existed in several container
|
||||||
|
runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers
|
||||||
|
that can modify path components after `SecureJoin` returns and before the
|
||||||
|
caller uses the path, allowing for some fairly trivial TOCTOU attacks.
|
||||||
|
|
||||||
```go
|
`SecureJoin` (and `SecureJoinVFS`) are still provided by this library to
|
||||||
func SecureJoin(root, unsafePath string) (string, error)
|
support legacy users, but new users are strongly suggested to avoid using
|
||||||
```
|
`SecureJoin` and instead use the [new api](#new-api) or switch to
|
||||||
|
[libpathrs][libpathrs].
|
||||||
|
|
||||||
This library **guarantees** the following:
|
With the above limitations in mind, this library guarantees the following:
|
||||||
|
|
||||||
* If no error is set, the resulting string **must** be a child path of
|
* If no error is set, the resulting string **must** be a child path of
|
||||||
`root` and will not contain any symlink path components (they will all be
|
`root` and will not contain any symlink path components (they will all be
|
||||||
|
|
@ -47,7 +41,7 @@ This library **guarantees** the following:
|
||||||
A (trivial) implementation of this function on GNU/Linux systems could be done
|
A (trivial) implementation of this function on GNU/Linux systems could be done
|
||||||
with the following (note that this requires root privileges and is far more
|
with the following (note that this requires root privileges and is far more
|
||||||
opaque than the implementation in this library, and also requires that
|
opaque than the implementation in this library, and also requires that
|
||||||
`readlink` is inside the `root` path):
|
`readlink` is inside the `root` path and is trustworthy):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package securejoin
|
package securejoin
|
||||||
|
|
@ -70,9 +64,105 @@ func SecureJoin(root, unsafePath string) (string, error) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[lwn-obeneath]: https://lwn.net/Articles/767547/
|
[libpathrs]: https://github.com/openSUSE/libpathrs
|
||||||
[go#20126]: https://github.com/golang/go/issues/20126
|
[go#20126]: https://github.com/golang/go/issues/20126
|
||||||
|
|
||||||
|
### New API ###
|
||||||
|
|
||||||
|
While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
|
||||||
|
stable release, some methods implemented by libpathrs have been ported to this
|
||||||
|
library to ease the transition. These APIs are only supported on Linux.
|
||||||
|
|
||||||
|
These APIs are implemented such that `filepath-securejoin` will
|
||||||
|
opportunistically use certain newer kernel APIs that make these operations far
|
||||||
|
more secure. In particular:
|
||||||
|
|
||||||
|
* All of the lookup operations will use [`openat2`][openat2.2] on new enough
|
||||||
|
kernels (Linux 5.6 or later) to restrict lookups through magic-links and
|
||||||
|
bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to
|
||||||
|
efficiently resolve symlinks within a rootfs.
|
||||||
|
|
||||||
|
* The APIs provide hardening against a malicious `/proc` mount to either detect
|
||||||
|
or avoid being tricked by a `/proc` that is not legitimate. This is done
|
||||||
|
using [`openat2`][openat2.2] for all users, and privileged users will also be
|
||||||
|
further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2]
|
||||||
|
(Linux 5.2 or later).
|
||||||
|
|
||||||
|
[openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html
|
||||||
|
[fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md
|
||||||
|
[open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md
|
||||||
|
|
||||||
|
#### `OpenInRoot` ####
|
||||||
|
|
||||||
|
```go
|
||||||
|
func OpenInRoot(root, unsafePath string) (*os.File, error)
|
||||||
|
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)
|
||||||
|
func Reopen(handle *os.File, flags int) (*os.File, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
`OpenInRoot` is a much safer version of
|
||||||
|
|
||||||
|
```go
|
||||||
|
path, err := securejoin.SecureJoin(root, unsafePath)
|
||||||
|
file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
|
||||||
|
```
|
||||||
|
|
||||||
|
that protects against various race attacks that could lead to serious security
|
||||||
|
issues, depending on the application. Note that the returned `*os.File` is an
|
||||||
|
`O_PATH` file descriptor, which is quite restricted. Callers will probably need
|
||||||
|
to use `Reopen` to get a more usable handle (this split is done to provide
|
||||||
|
useful features like PTY spawning and to avoid users accidentally opening bad
|
||||||
|
inodes that could cause a DoS).
|
||||||
|
|
||||||
|
Callers need to be careful in how they use the returned `*os.File`. Usually it
|
||||||
|
is only safe to operate on the handle directly, and it is very easy to create a
|
||||||
|
security issue. [libpathrs][libpathrs] provides far more helpers to make using
|
||||||
|
these handles safer -- there is currently no plan to port them to
|
||||||
|
`filepath-securejoin`.
|
||||||
|
|
||||||
|
`OpenatInRoot` is like `OpenInRoot` except that the root is provided using an
|
||||||
|
`*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or
|
||||||
|
`MkdirAllHandle`) calls are operating on the same rootfs.
|
||||||
|
|
||||||
|
> **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits
|
||||||
|
> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
|
||||||
|
> which treated non-existent components as though they were real directories,
|
||||||
|
> and would allow for partial resolution of dangling symlinks. These behaviours
|
||||||
|
> are at odds with how Linux treats non-existent paths and dangling symlinks,
|
||||||
|
> and so these are no longer allowed.
|
||||||
|
|
||||||
|
#### `MkdirAll` ####
|
||||||
|
|
||||||
|
```go
|
||||||
|
func MkdirAll(root, unsafePath string, mode int) error
|
||||||
|
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
`MkdirAll` is a much safer version of
|
||||||
|
|
||||||
|
```go
|
||||||
|
path, err := securejoin.SecureJoin(root, unsafePath)
|
||||||
|
err = os.MkdirAll(path, mode)
|
||||||
|
```
|
||||||
|
|
||||||
|
that protects against the same kinds of races that `OpenInRoot` protects
|
||||||
|
against.
|
||||||
|
|
||||||
|
`MkdirAllHandle` is like `MkdirAll` except that the root is provided using an
|
||||||
|
`*os.File` (the reason for this is the same as with `OpenatInRoot`) and an
|
||||||
|
`*os.File` of the final created directory is returned (this directory is
|
||||||
|
guaranteed to be effectively identical to the directory created by
|
||||||
|
`MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot`
|
||||||
|
after `MkdirAll`).
|
||||||
|
|
||||||
|
> **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits
|
||||||
|
> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
|
||||||
|
> which treated non-existent components as though they were real directories,
|
||||||
|
> and would allow for partial resolution of dangling symlinks. These behaviours
|
||||||
|
> are at odds with how Linux treats non-existent paths and dangling symlinks,
|
||||||
|
> and so these are no longer allowed. This means that `MkdirAll` will not
|
||||||
|
> create non-existent directories referenced by a dangling symlink.
|
||||||
|
|
||||||
### License ###
|
### License ###
|
||||||
|
|
||||||
The license of this project is the same as Go, which is a BSD 3-clause license
|
The license of this project is the same as Go, which is a BSD 3-clause license
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
0.2.4
|
0.3.6
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
||||||
|
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package securejoin implements a set of helpers to make it easier to write Go
|
||||||
|
// code that is safe against symlink-related escape attacks. The primary idea
|
||||||
|
// is to let you resolve a path within a rootfs directory as if the rootfs was
|
||||||
|
// a chroot.
|
||||||
|
//
|
||||||
|
// securejoin has two APIs, a "legacy" API and a "modern" API.
|
||||||
|
//
|
||||||
|
// The legacy API is [SecureJoin] and [SecureJoinVFS]. These methods are
|
||||||
|
// **not** safe against race conditions where an attacker changes the
|
||||||
|
// filesystem after (or during) the [SecureJoin] operation.
|
||||||
|
//
|
||||||
|
// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived
|
||||||
|
// functions). These are safe against racing attackers and have several other
|
||||||
|
// protections that are not provided by the legacy API. There are many more
|
||||||
|
// operations that most programs expect to be able to do safely, but we do not
|
||||||
|
// provide explicit support for them because we want to encourage users to
|
||||||
|
// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a
|
||||||
|
// cross-language next-generation library that is entirely designed around
|
||||||
|
// operating on paths safely.
|
||||||
|
//
|
||||||
|
// securejoin has been used by several container runtimes (Docker, runc,
|
||||||
|
// Kubernetes, etc) for quite a few years as a de-facto standard for operating
|
||||||
|
// on container filesystem paths "safely". However, most users still use the
|
||||||
|
// legacy API which is unsafe against various attacks (there is a fairly long
|
||||||
|
// history of CVEs in dependent as a result). Users should switch to the modern
|
||||||
|
// API as soon as possible (or even better, switch to libpathrs).
|
||||||
|
//
|
||||||
|
// This project was initially intended to be included in the Go standard
|
||||||
|
// library, but [it was rejected](https://go.dev/issue/20126). There is now a
|
||||||
|
// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API
|
||||||
|
// that shares some of the goals of filepath-securejoin. However, that design
|
||||||
|
// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the
|
||||||
|
// usecase of container runtimes and most system tools.
|
||||||
|
package securejoin
|
||||||
18
vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_go120.go
generated
vendored
Normal file
18
vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_go120.go
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
//go:build linux && go1.20
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
||||||
|
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
||||||
|
// is only guaranteed to give you baseErr.
|
||||||
|
func wrapBaseError(baseErr, extraErr error) error {
|
||||||
|
return fmt.Errorf("%w: %w", extraErr, baseErr)
|
||||||
|
}
|
||||||
38
vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_unsupported.go
generated
vendored
Normal file
38
vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_unsupported.go
generated
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
//go:build linux && !go1.20
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrappedError struct {
|
||||||
|
inner error
|
||||||
|
isError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err wrappedError) Is(target error) bool {
|
||||||
|
return err.isError == target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err wrappedError) Unwrap() error {
|
||||||
|
return err.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err wrappedError) Error() string {
|
||||||
|
return fmt.Sprintf("%v: %v", err.isError, err.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
||||||
|
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
||||||
|
// is only guaranteed to give you baseErr.
|
||||||
|
func wrapBaseError(baseErr, extraErr error) error {
|
||||||
|
return wrappedError{
|
||||||
|
inner: baseErr,
|
||||||
|
isError: extraErr,
|
||||||
|
}
|
||||||
|
}
|
||||||
32
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go
generated
vendored
Normal file
32
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go
generated
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
//go:build linux && go1.21
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func slices_DeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
|
||||||
|
return slices.DeleteFunc(slice, delFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func slices_Contains[S ~[]E, E comparable](slice S, val E) bool {
|
||||||
|
return slices.Contains(slice, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func slices_Clone[S ~[]E, E any](slice S) S {
|
||||||
|
return slices.Clone(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sync_OnceValue[T any](f func() T) func() T {
|
||||||
|
return sync.OnceValue(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||||
|
return sync.OnceValues(f)
|
||||||
|
}
|
||||||
124
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go
generated
vendored
Normal file
124
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go
generated
vendored
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
//go:build linux && !go1.21
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are very minimal implementations of functions that appear in Go 1.21's
|
||||||
|
// stdlib, included so that we can build on older Go versions. Most are
|
||||||
|
// borrowed directly from the stdlib, and a few are modified to be "obviously
|
||||||
|
// correct" without needing to copy too many other helpers.
|
||||||
|
|
||||||
|
// clearSlice is equivalent to the builtin clear from Go 1.21.
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func clearSlice[S ~[]E, E any](slice S) {
|
||||||
|
var zero E
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func slices_IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||||
|
for i := range s {
|
||||||
|
if f(s[i]) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func slices_DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
||||||
|
i := slices_IndexFunc(s, del)
|
||||||
|
if i == -1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
// Don't start copying elements until we find one to delete.
|
||||||
|
for j := i + 1; j < len(s); j++ {
|
||||||
|
if v := s[j]; !del(v) {
|
||||||
|
s[i] = v
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to the stdlib slices.Contains, except that we don't have
|
||||||
|
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
|
||||||
|
func slices_Contains[S ~[]E, E comparable](s S, v E) bool {
|
||||||
|
return slices_IndexFunc(s, func(e E) bool { return e == v }) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func slices_Clone[S ~[]E, E any](s S) S {
|
||||||
|
// Preserve nil in case it matters.
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return append(S([]E{}), s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func sync_OnceValue[T any](f func() T) func() T {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
result T
|
||||||
|
)
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
result = f()
|
||||||
|
f = nil
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
return func() T {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
r1 T1
|
||||||
|
r2 T2
|
||||||
|
)
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
r1, r2 = f()
|
||||||
|
f = nil
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
return func() (T1, T2) {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
return r1, r2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
||||||
// Copyright (C) 2017 SUSE LLC. All rights reserved.
|
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package securejoin is an implementation of the hopefully-soon-to-be-included
|
|
||||||
// SecureJoin helper that is meant to be part of the "path/filepath" package.
|
|
||||||
// The purpose of this project is to provide a PoC implementation to make the
|
|
||||||
// SecureJoin proposal (https://github.com/golang/go/issues/20126) more
|
|
||||||
// tangible.
|
|
||||||
package securejoin
|
package securejoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -19,26 +13,34 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxSymlinkLimit = 255
|
||||||
|
|
||||||
// IsNotExist tells you if err is an error that implies that either the path
|
// IsNotExist tells you if err is an error that implies that either the path
|
||||||
// accessed does not exist (or path components don't exist). This is
|
// accessed does not exist (or path components don't exist). This is
|
||||||
// effectively a more broad version of os.IsNotExist.
|
// effectively a more broad version of [os.IsNotExist].
|
||||||
func IsNotExist(err error) bool {
|
func IsNotExist(err error) bool {
|
||||||
// Check that it's not actually an ENOTDIR, which in some cases is a more
|
// Check that it's not actually an ENOTDIR, which in some cases is a more
|
||||||
// convoluted case of ENOENT (usually involving weird paths).
|
// convoluted case of ENOENT (usually involving weird paths).
|
||||||
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
|
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureJoinVFS joins the two given path components (similar to Join) except
|
// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except
|
||||||
// that the returned path is guaranteed to be scoped inside the provided root
|
// that the returned path is guaranteed to be scoped inside the provided root
|
||||||
// path (when evaluated). Any symbolic links in the path are evaluated with the
|
// path (when evaluated). Any symbolic links in the path are evaluated with the
|
||||||
// given root treated as the root of the filesystem, similar to a chroot. The
|
// given root treated as the root of the filesystem, similar to a chroot. The
|
||||||
// filesystem state is evaluated through the given VFS interface (if nil, the
|
// filesystem state is evaluated through the given [VFS] interface (if nil, the
|
||||||
// standard os.* family of functions are used).
|
// standard [os].* family of functions are used).
|
||||||
//
|
//
|
||||||
// Note that the guarantees provided by this function only apply if the path
|
// Note that the guarantees provided by this function only apply if the path
|
||||||
// components in the returned string are not modified (in other words are not
|
// components in the returned string are not modified (in other words are not
|
||||||
// replaced with symlinks on the filesystem) after this function has returned.
|
// replaced with symlinks on the filesystem) after this function has returned.
|
||||||
// Such a symlink race is necessarily out-of-scope of SecureJoin.
|
// Such a symlink race is necessarily out-of-scope of SecureJoinVFS.
|
||||||
|
//
|
||||||
|
// NOTE: Due to the above limitation, Linux users are strongly encouraged to
|
||||||
|
// use [OpenInRoot] instead, which does safely protect against these kinds of
|
||||||
|
// attacks. There is no way to solve this problem with SecureJoinVFS because
|
||||||
|
// the API is fundamentally wrong (you cannot return a "safe" path string and
|
||||||
|
// guarantee it won't be modified afterwards).
|
||||||
//
|
//
|
||||||
// Volume names in unsafePath are always discarded, regardless if they are
|
// Volume names in unsafePath are always discarded, regardless if they are
|
||||||
// provided via direct input or when evaluating symlinks. Therefore:
|
// provided via direct input or when evaluating symlinks. Therefore:
|
||||||
|
|
@ -51,75 +53,73 @@ func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafePath = filepath.FromSlash(unsafePath)
|
unsafePath = filepath.FromSlash(unsafePath)
|
||||||
var path bytes.Buffer
|
var (
|
||||||
n := 0
|
currentPath string
|
||||||
for unsafePath != "" {
|
remainingPath = unsafePath
|
||||||
if n > 255 {
|
linksWalked int
|
||||||
return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
|
)
|
||||||
|
for remainingPath != "" {
|
||||||
|
if v := filepath.VolumeName(remainingPath); v != "" {
|
||||||
|
remainingPath = remainingPath[len(v):]
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := filepath.VolumeName(unsafePath); v != "" {
|
// Get the next path component.
|
||||||
unsafePath = unsafePath[len(v):]
|
var part string
|
||||||
}
|
if i := strings.IndexRune(remainingPath, filepath.Separator); i == -1 {
|
||||||
|
part, remainingPath = remainingPath, ""
|
||||||
// Next path component, p.
|
|
||||||
i := strings.IndexRune(unsafePath, filepath.Separator)
|
|
||||||
var p string
|
|
||||||
if i == -1 {
|
|
||||||
p, unsafePath = unsafePath, ""
|
|
||||||
} else {
|
} else {
|
||||||
p, unsafePath = unsafePath[:i], unsafePath[i+1:]
|
part, remainingPath = remainingPath[:i], remainingPath[i+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cleaned path, using the lexical semantics of /../a, to
|
// Apply the component lexically to the path we are building.
|
||||||
// create a "scoped" path component which can safely be joined to fullP
|
// currentPath does not contain any symlinks, and we are lexically
|
||||||
// for evaluation. At this point, path.String() doesn't contain any
|
// dealing with a single component, so it's okay to do a filepath.Clean
|
||||||
// symlink components.
|
// here.
|
||||||
cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p)
|
nextPath := filepath.Join(string(filepath.Separator), currentPath, part)
|
||||||
if cleanP == string(filepath.Separator) {
|
if nextPath == string(filepath.Separator) {
|
||||||
path.Reset()
|
currentPath = ""
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fullP := filepath.Clean(root + cleanP)
|
fullPath := root + string(filepath.Separator) + nextPath
|
||||||
|
|
||||||
// Figure out whether the path is a symlink.
|
// Figure out whether the path is a symlink.
|
||||||
fi, err := vfs.Lstat(fullP)
|
fi, err := vfs.Lstat(fullPath)
|
||||||
if err != nil && !IsNotExist(err) {
|
if err != nil && !IsNotExist(err) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Treat non-existent path components the same as non-symlinks (we
|
// Treat non-existent path components the same as non-symlinks (we
|
||||||
// can't do any better here).
|
// can't do any better here).
|
||||||
if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 {
|
if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 {
|
||||||
path.WriteString(p)
|
currentPath = nextPath
|
||||||
path.WriteRune(filepath.Separator)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only increment when we actually dereference a link.
|
// It's a symlink, so get its contents and expand it by prepending it
|
||||||
n++
|
// to the yet-unparsed path.
|
||||||
|
linksWalked++
|
||||||
|
if linksWalked > maxSymlinkLimit {
|
||||||
|
return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
|
||||||
|
}
|
||||||
|
|
||||||
// It's a symlink, expand it by prepending it to the yet-unparsed path.
|
dest, err := vfs.Readlink(fullPath)
|
||||||
dest, err := vfs.Readlink(fullP)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
remainingPath = dest + string(filepath.Separator) + remainingPath
|
||||||
// Absolute symlinks reset any work we've already done.
|
// Absolute symlinks reset any work we've already done.
|
||||||
if filepath.IsAbs(dest) {
|
if filepath.IsAbs(dest) {
|
||||||
path.Reset()
|
currentPath = ""
|
||||||
}
|
}
|
||||||
unsafePath = dest + string(filepath.Separator) + unsafePath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have to clean path.String() here because it may contain '..'
|
// There should be no lexical components like ".." left in the path here,
|
||||||
// components that are entirely lexical, but would be misleading otherwise.
|
// but for safety clean up the path before joining it to the root.
|
||||||
// And finally do a final clean to ensure that root is also lexically
|
finalPath := filepath.Join(string(filepath.Separator), currentPath)
|
||||||
// clean.
|
return filepath.Join(root, finalPath), nil
|
||||||
fullP := filepath.Clean(string(filepath.Separator) + path.String())
|
|
||||||
return filepath.Clean(root + fullP), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library
|
// SecureJoin is a wrapper around [SecureJoinVFS] that just uses the [os].* library
|
||||||
// of functions as the VFS. If in doubt, use this function over SecureJoinVFS.
|
// of functions as the [VFS]. If in doubt, use this function over [SecureJoinVFS].
|
||||||
func SecureJoin(root, unsafePath string) (string, error) {
|
func SecureJoin(root, unsafePath string) (string, error) {
|
||||||
return SecureJoinVFS(root, unsafePath, nil)
|
return SecureJoinVFS(root, unsafePath, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type symlinkStackEntry struct {
|
||||||
|
// (dir, remainingPath) is what we would've returned if the link didn't
|
||||||
|
// exist. This matches what openat2(RESOLVE_IN_ROOT) would return in
|
||||||
|
// this case.
|
||||||
|
dir *os.File
|
||||||
|
remainingPath string
|
||||||
|
// linkUnwalked is the remaining path components from the original
|
||||||
|
// Readlink which we have yet to walk. When this slice is empty, we
|
||||||
|
// drop the link from the stack.
|
||||||
|
linkUnwalked []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (se symlinkStackEntry) String() string {
|
||||||
|
return fmt.Sprintf("<%s>/%s [->%s]", se.dir.Name(), se.remainingPath, strings.Join(se.linkUnwalked, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (se symlinkStackEntry) Close() {
|
||||||
|
_ = se.dir.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type symlinkStack []*symlinkStackEntry
|
||||||
|
|
||||||
|
func (s *symlinkStack) IsEmpty() bool {
|
||||||
|
return s == nil || len(*s) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *symlinkStack) Close() {
|
||||||
|
if s != nil {
|
||||||
|
for _, link := range *s {
|
||||||
|
link.Close()
|
||||||
|
}
|
||||||
|
// TODO: Switch to clear once we switch to Go 1.21.
|
||||||
|
*s = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errEmptyStack = errors.New("[internal] stack is empty")
|
||||||
|
errBrokenSymlinkStack = errors.New("[internal error] broken symlink stack")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *symlinkStack) popPart(part string) error {
|
||||||
|
if s == nil || s.IsEmpty() {
|
||||||
|
// If there is nothing in the symlink stack, then the part was from the
|
||||||
|
// real path provided by the user, and this is a no-op.
|
||||||
|
return errEmptyStack
|
||||||
|
}
|
||||||
|
if part == "." {
|
||||||
|
// "." components are no-ops -- we drop them when doing SwapLink.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tailEntry := (*s)[len(*s)-1]
|
||||||
|
|
||||||
|
// Double-check that we are popping the component we expect.
|
||||||
|
if len(tailEntry.linkUnwalked) == 0 {
|
||||||
|
return fmt.Errorf("%w: trying to pop component %q of empty stack entry %s", errBrokenSymlinkStack, part, tailEntry)
|
||||||
|
}
|
||||||
|
headPart := tailEntry.linkUnwalked[0]
|
||||||
|
if headPart != part {
|
||||||
|
return fmt.Errorf("%w: trying to pop component %q but the last stack entry is %s (%q)", errBrokenSymlinkStack, part, tailEntry, headPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the component, but keep the entry around in case we are dealing
|
||||||
|
// with a "tail-chained" symlink.
|
||||||
|
tailEntry.linkUnwalked = tailEntry.linkUnwalked[1:]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *symlinkStack) PopPart(part string) error {
|
||||||
|
if err := s.popPart(part); err != nil {
|
||||||
|
if errors.Is(err, errEmptyStack) {
|
||||||
|
// Skip empty stacks.
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any of the trailing stack entries that are empty.
|
||||||
|
for lastGood := len(*s) - 1; lastGood >= 0; lastGood-- {
|
||||||
|
entry := (*s)[lastGood]
|
||||||
|
if len(entry.linkUnwalked) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
entry.Close()
|
||||||
|
(*s) = (*s)[:lastGood]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Split the link target and clean up any "" parts.
|
||||||
|
linkTargetParts := slices_DeleteFunc(
|
||||||
|
strings.Split(linkTarget, "/"),
|
||||||
|
func(part string) bool { return part == "" || part == "." })
|
||||||
|
|
||||||
|
// Copy the directory so the caller doesn't close our copy.
|
||||||
|
dirCopy, err := dupFile(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the stack.
|
||||||
|
*s = append(*s, &symlinkStackEntry{
|
||||||
|
dir: dirCopy,
|
||||||
|
remainingPath: remainingPath,
|
||||||
|
linkUnwalked: linkTargetParts,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, linkTarget string) error {
|
||||||
|
// If we are currently inside a symlink resolution, remove the symlink
|
||||||
|
// component from the last symlink entry, but don't remove the entry even
|
||||||
|
// if it's empty. If we are a "tail-chained" symlink (a trailing symlink we
|
||||||
|
// hit during a symlink resolution) we need to keep the old symlink until
|
||||||
|
// we finish the resolution.
|
||||||
|
if err := s.popPart(linkPart); err != nil {
|
||||||
|
if !errors.Is(err, errEmptyStack) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Push the component regardless of whether the stack was empty.
|
||||||
|
}
|
||||||
|
return s.push(dir, remainingPath, linkTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
|
||||||
|
if s == nil || s.IsEmpty() {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
tailEntry := (*s)[0]
|
||||||
|
*s = (*s)[1:]
|
||||||
|
return tailEntry.dir, tailEntry.remainingPath, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialLookupInRoot tries to lookup as much of the request path as possible
|
||||||
|
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
|
||||||
|
// component of the requested path, returning a file handle to the final
|
||||||
|
// existing component and a string containing the remaining path components.
|
||||||
|
func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) {
|
||||||
|
return lookupInRoot(root, unsafePath, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||||
|
handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
|
||||||
|
if remainingPath != "" && err == nil {
|
||||||
|
// should never happen
|
||||||
|
err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath)
|
||||||
|
}
|
||||||
|
// lookupInRoot(partial=false) will always close the handle if an error is
|
||||||
|
// returned, so no need to double-check here.
|
||||||
|
return handle, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
|
||||||
|
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||||
|
|
||||||
|
// This is very similar to SecureJoin, except that we operate on the
|
||||||
|
// components using file descriptors. We then return the last component we
|
||||||
|
// managed open, along with the remaining path components not opened.
|
||||||
|
|
||||||
|
// Try to use openat2 if possible.
|
||||||
|
if hasOpenat2() {
|
||||||
|
return lookupOpenat2(root, unsafePath, partial)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the "actual" root path from /proc/self/fd. This is necessary if the
|
||||||
|
// root is some magic-link like /proc/$pid/root, in which case we want to
|
||||||
|
// make sure when we do checkProcSelfFdPath that we are using the correct
|
||||||
|
// root path.
|
||||||
|
logicalRootPath, err := procSelfFdReadlink(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("get real root path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir, err := dupFile(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// If a handle is not returned, close the internal handle.
|
||||||
|
if Handle == nil {
|
||||||
|
_ = currentDir.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// symlinkStack is used to emulate how openat2(RESOLVE_IN_ROOT) treats
|
||||||
|
// dangling symlinks. If we hit a non-existent path while resolving a
|
||||||
|
// symlink, we need to return the (dir, remainingPath) that we had when we
|
||||||
|
// hit the symlink (treating the symlink as though it were a regular file).
|
||||||
|
// The set of (dir, remainingPath) sets is stored within the symlinkStack
|
||||||
|
// and we add and remove parts when we hit symlink and non-symlink
|
||||||
|
// components respectively. We need a stack because of recursive symlinks
|
||||||
|
// (symlinks that contain symlink components in their target).
|
||||||
|
//
|
||||||
|
// Note that the stack is ONLY used for book-keeping. All of the actual
|
||||||
|
// path walking logic is still based on currentPath/remainingPath and
|
||||||
|
// currentDir (as in SecureJoin).
|
||||||
|
var symStack *symlinkStack
|
||||||
|
if partial {
|
||||||
|
symStack = new(symlinkStack)
|
||||||
|
defer symStack.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
linksWalked int
|
||||||
|
currentPath string
|
||||||
|
remainingPath = unsafePath
|
||||||
|
)
|
||||||
|
for remainingPath != "" {
|
||||||
|
// Save the current remaining path so if the part is not real we can
|
||||||
|
// return the path including the component.
|
||||||
|
oldRemainingPath := remainingPath
|
||||||
|
|
||||||
|
// Get the next path component.
|
||||||
|
var part string
|
||||||
|
if i := strings.IndexByte(remainingPath, '/'); i == -1 {
|
||||||
|
part, remainingPath = remainingPath, ""
|
||||||
|
} else {
|
||||||
|
part, remainingPath = remainingPath[:i], remainingPath[i+1:]
|
||||||
|
}
|
||||||
|
// If we hit an empty component, we need to treat it as though it is
|
||||||
|
// "." so that trailing "/" and "//" components on a non-directory
|
||||||
|
// correctly return the right error code.
|
||||||
|
if part == "" {
|
||||||
|
part = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the component lexically to the path we are building.
|
||||||
|
// currentPath does not contain any symlinks, and we are lexically
|
||||||
|
// dealing with a single component, so it's okay to do a filepath.Clean
|
||||||
|
// here.
|
||||||
|
nextPath := path.Join("/", currentPath, part)
|
||||||
|
// If we logically hit the root, just clone the root rather than
|
||||||
|
// opening the part and doing all of the other checks.
|
||||||
|
if nextPath == "/" {
|
||||||
|
if err := symStack.PopPart(part); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
|
||||||
|
}
|
||||||
|
// Jump to root.
|
||||||
|
rootClone, err := dupFile(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||||
|
}
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = rootClone
|
||||||
|
currentPath = nextPath
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to open the next component.
|
||||||
|
nextDir, err := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
st, err := nextDir.Stat()
|
||||||
|
if err != nil {
|
||||||
|
_ = nextDir.Close()
|
||||||
|
return nil, "", fmt.Errorf("stat component %q: %w", part, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch st.Mode() & os.ModeType {
|
||||||
|
case os.ModeSymlink:
|
||||||
|
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
|
||||||
|
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
|
||||||
|
// fstatat() with empty relative pathnames").
|
||||||
|
linkDest, err := readlinkatFile(nextDir, "")
|
||||||
|
// We don't need the handle anymore.
|
||||||
|
_ = nextDir.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
linksWalked++
|
||||||
|
if linksWalked > maxSymlinkLimit {
|
||||||
|
return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap out the symlink's component for the link entry itself.
|
||||||
|
if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update our logical remaining path.
|
||||||
|
remainingPath = linkDest + "/" + remainingPath
|
||||||
|
// Absolute symlinks reset any work we've already done.
|
||||||
|
if path.IsAbs(linkDest) {
|
||||||
|
// Jump to root.
|
||||||
|
rootClone, err := dupFile(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||||
|
}
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = rootClone
|
||||||
|
currentPath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// If we are dealing with a directory, simply walk into it.
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = nextDir
|
||||||
|
currentPath = nextPath
|
||||||
|
|
||||||
|
// The part was real, so drop it from the symlink stack.
|
||||||
|
if err := symStack.PopPart(part); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are operating on a .., make sure we haven't escaped.
|
||||||
|
// We only have to check for ".." here because walking down
|
||||||
|
// into a regular component component cannot cause you to
|
||||||
|
// escape. This mirrors the logic in RESOLVE_IN_ROOT, except we
|
||||||
|
// have to check every ".." rather than only checking after a
|
||||||
|
// rename or mount on the system.
|
||||||
|
if part == ".." {
|
||||||
|
// Make sure the root hasn't moved.
|
||||||
|
if err := checkProcSelfFdPath(logicalRootPath, root); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
|
||||||
|
}
|
||||||
|
// Make sure the path is what we expect.
|
||||||
|
fullPath := logicalRootPath + nextPath
|
||||||
|
if err := checkProcSelfFdPath(fullPath, currentDir); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if !partial {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
// If there are any remaining components in the symlink stack, we
|
||||||
|
// are still within a symlink resolution and thus we hit a dangling
|
||||||
|
// symlink. So pretend that the first symlink in the stack we hit
|
||||||
|
// was an ENOENT (to match openat2).
|
||||||
|
if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok {
|
||||||
|
_ = currentDir.Close()
|
||||||
|
return oldDir, remainingPath, err
|
||||||
|
}
|
||||||
|
// We have hit a final component that doesn't exist, so we have our
|
||||||
|
// partial open result. Note that we have to use the OLD remaining
|
||||||
|
// path, since the lookup failed.
|
||||||
|
return currentDir, oldRemainingPath, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the unsafePath had a trailing slash, we need to make sure we try to
|
||||||
|
// do a relative "." open so that we will correctly return an error when
|
||||||
|
// the final component is a non-directory (to match openat2). In the
|
||||||
|
// context of openat2, a trailing slash and a trailing "/." are completely
|
||||||
|
// equivalent.
|
||||||
|
if strings.HasSuffix(unsafePath, "/") {
|
||||||
|
nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
if !partial {
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = nil
|
||||||
|
}
|
||||||
|
return currentDir, "", err
|
||||||
|
}
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = nextDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// All of the components existed!
|
||||||
|
return currentDir, "", nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidMode = errors.New("invalid permission mode")
|
||||||
|
errPossibleAttack = errors.New("possible attack detected")
|
||||||
|
)
|
||||||
|
|
||||||
|
// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
|
||||||
|
// in two respects:
|
||||||
|
//
|
||||||
|
// - The caller provides the root directory as an *[os.File] (preferably O_PATH)
|
||||||
|
// handle. This means that the caller can be sure which root directory is
|
||||||
|
// being used. Note that this can be emulated by using /proc/self/fd/... as
|
||||||
|
// the root path with [os.MkdirAll].
|
||||||
|
//
|
||||||
|
// - Once all of the directories have been created, an *[os.File] O_PATH handle
|
||||||
|
// to the directory at unsafePath is returned to the caller. This is done in
|
||||||
|
// an effectively-race-free way (an attacker would only be able to swap the
|
||||||
|
// final directory component), which is not possible to emulate with
|
||||||
|
// [MkdirAll].
|
||||||
|
//
|
||||||
|
// In addition, the returned handle is obtained far more efficiently than doing
|
||||||
|
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
|
||||||
|
// doing [MkdirAll]. If you intend to open the directory after creating it, you
|
||||||
|
// should use MkdirAllHandle.
|
||||||
|
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err error) {
|
||||||
|
// Make sure there are no os.FileMode bits set.
|
||||||
|
if mode&^0o7777 != 0 {
|
||||||
|
return nil, fmt.Errorf("%w for mkdir 0o%.3o", errInvalidMode, mode)
|
||||||
|
}
|
||||||
|
// On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid
|
||||||
|
// bits. We could also silently ignore them but since we have very few
|
||||||
|
// users it seems more prudent to return an error so users notice that
|
||||||
|
// these bits will not be set.
|
||||||
|
if mode&^0o1777 != 0 {
|
||||||
|
return nil, fmt.Errorf("%w for mkdir 0o%.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to open as much of the path as possible.
|
||||||
|
currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = currentDir.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil && !errors.Is(err, unix.ENOENT) {
|
||||||
|
return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is an attacker deleting directories as we walk into them,
|
||||||
|
// detect this proactively. Note this is guaranteed to detect if the
|
||||||
|
// attacker deleted any part of the tree up to currentDir.
|
||||||
|
//
|
||||||
|
// Once we walk into a dead directory, partialLookupInRoot would not be
|
||||||
|
// able to walk further down the tree (directories must be empty before
|
||||||
|
// they are deleted), and if the attacker has removed the entire tree we
|
||||||
|
// can be sure that anything that was originally inside a dead directory
|
||||||
|
// must also be deleted and thus is a dead directory in its own right.
|
||||||
|
//
|
||||||
|
// This is mostly a quality-of-life check, because mkdir will simply fail
|
||||||
|
// later if the attacker deletes the tree after this check.
|
||||||
|
if err := isDeadInode(currentDir); err != nil {
|
||||||
|
return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
|
||||||
|
// always return a non-O_PATH handle). We also check that we actually got a
|
||||||
|
// directory.
|
||||||
|
if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
|
||||||
|
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
|
||||||
|
} else {
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = reopenDir
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingParts := strings.Split(remainingPath, string(filepath.Separator))
|
||||||
|
if slices_Contains(remainingParts, "..") {
|
||||||
|
// The path contained ".." components after the end of the "real"
|
||||||
|
// components. We could try to safely resolve ".." here but that would
|
||||||
|
// add a bunch of extra logic for something that it's not clear even
|
||||||
|
// needs to be supported. So just return an error.
|
||||||
|
//
|
||||||
|
// If we do filepath.Clean(remainingPath) then we end up with the
|
||||||
|
// problem that ".." can erase a trailing dangling symlink and produce
|
||||||
|
// a path that doesn't quite match what the user asked for.
|
||||||
|
return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the mode doesn't have any type bits.
|
||||||
|
mode &^= unix.S_IFMT
|
||||||
|
|
||||||
|
// Create the remaining components.
|
||||||
|
for _, part := range remainingParts {
|
||||||
|
switch part {
|
||||||
|
case "", ".":
|
||||||
|
// Skip over no-op paths.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: mkdir(2) will not follow trailing symlinks, so we can safely
|
||||||
|
// create the final component without worrying about symlink-exchange
|
||||||
|
// attacks.
|
||||||
|
//
|
||||||
|
// If we get -EEXIST, it's possible that another program created the
|
||||||
|
// directory at the same time as us. In that case, just continue on as
|
||||||
|
// if we created it (if the created inode is not a directory, the
|
||||||
|
// following open call will fail).
|
||||||
|
if err := unix.Mkdirat(int(currentDir.Fd()), part, uint32(mode)); err != nil && !errors.Is(err, unix.EEXIST) {
|
||||||
|
err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
|
||||||
|
// Make the error a bit nicer if the directory is dead.
|
||||||
|
if deadErr := isDeadInode(currentDir); deadErr != nil {
|
||||||
|
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||||
|
// multiple %w verbs for this wrapping. For now we need to use a
|
||||||
|
// compatibility shim for older Go versions.
|
||||||
|
//err = fmt.Errorf("%w (%w)", err, deadErr)
|
||||||
|
err = wrapBaseError(err, deadErr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a handle to the next component. O_DIRECTORY means we don't need
|
||||||
|
// to use O_PATH.
|
||||||
|
var nextDir *os.File
|
||||||
|
if hasOpenat2() {
|
||||||
|
nextDir, err = openat2File(currentDir, part, &unix.OpenHow{
|
||||||
|
Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = currentDir.Close()
|
||||||
|
currentDir = nextDir
|
||||||
|
|
||||||
|
// It's possible that the directory we just opened was swapped by an
|
||||||
|
// attacker. Unfortunately there isn't much we can do to protect
|
||||||
|
// against this, and MkdirAll's behaviour is that we will reuse
|
||||||
|
// existing directories anyway so the need to protect against this is
|
||||||
|
// incredibly limited (and arguably doesn't even deserve mention here).
|
||||||
|
//
|
||||||
|
// Ideally we might want to check that the owner and mode match what we
|
||||||
|
// would've created -- unfortunately, it is non-trivial to verify that
|
||||||
|
// the owner and mode of the created directory match. While plain Unix
|
||||||
|
// DAC rules seem simple enough to emulate, there are a bunch of other
|
||||||
|
// factors that can change the mode or owner of created directories
|
||||||
|
// (default POSIX ACLs, mount options like uid=1,gid=2,umask=0 on
|
||||||
|
// filesystems like vfat, etc etc). We used to try to verify this but
|
||||||
|
// it just lead to a series of spurious errors.
|
||||||
|
//
|
||||||
|
// We could also check that the directory is non-empty, but
|
||||||
|
// unfortunately some pseduofilesystems (like cgroupfs) create
|
||||||
|
// non-empty directories, which would result in different spurious
|
||||||
|
// errors.
|
||||||
|
}
|
||||||
|
return currentDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
|
||||||
|
// where the new directory is guaranteed to be within the root directory (if an
|
||||||
|
// attacker can move directories from inside the root to outside the root, the
|
||||||
|
// created directory tree might be outside of the root but the key constraint
|
||||||
|
// is that at no point will we walk outside of the directory tree we are
|
||||||
|
// creating).
|
||||||
|
//
|
||||||
|
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
|
||||||
|
//
|
||||||
|
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||||
|
// err := os.MkdirAll(path, mode)
|
||||||
|
//
|
||||||
|
// But is much safer. The above implementation is unsafe because if an attacker
|
||||||
|
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
|
||||||
|
// possible for MkdirAll to resolve unsafe symlink components and create
|
||||||
|
// directories outside of the root.
|
||||||
|
//
|
||||||
|
// If you plan to open the directory after you have created it or want to use
|
||||||
|
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
|
||||||
|
// This function is a wrapper around [MkdirAllHandle].
|
||||||
|
//
|
||||||
|
// NOTE: The mode argument must be set the unix mode bits (unix.S_I...), not
|
||||||
|
// the Go generic mode bits ([os.FileMode]...).
|
||||||
|
func MkdirAll(root, unsafePath string, mode int) error {
|
||||||
|
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rootDir.Close()
|
||||||
|
|
||||||
|
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||||
|
// using an *[os.File] handle, to ensure that the correct root directory is used.
|
||||||
|
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||||
|
handle, err := completeLookupInRoot(root, unsafePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
|
||||||
|
}
|
||||||
|
return handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenInRoot safely opens the provided unsafePath within the root.
|
||||||
|
// Effectively, OpenInRoot(root, unsafePath) is equivalent to
|
||||||
|
//
|
||||||
|
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||||
|
// handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
|
||||||
|
//
|
||||||
|
// But is much safer. The above implementation is unsafe because if an attacker
|
||||||
|
// can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is
|
||||||
|
// possible for the returned file to be outside of the root.
|
||||||
|
//
|
||||||
|
// Note that the returned handle is an O_PATH handle, meaning that only a very
|
||||||
|
// limited set of operations will work on the handle. This is done to avoid
|
||||||
|
// accidentally opening an untrusted file that could cause issues (such as a
|
||||||
|
// disconnected TTY that could cause a DoS, or some other issue). In order to
|
||||||
|
// use the returned handle, you can "upgrade" it to a proper handle using
|
||||||
|
// [Reopen].
|
||||||
|
func OpenInRoot(root, unsafePath string) (*os.File, error) {
|
||||||
|
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rootDir.Close()
|
||||||
|
return OpenatInRoot(rootDir, unsafePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
|
||||||
|
// Reopen(file, flags) is effectively equivalent to
|
||||||
|
//
|
||||||
|
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
|
||||||
|
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
|
||||||
|
//
|
||||||
|
// But with some extra hardenings to ensure that we are not tricked by a
|
||||||
|
// maliciously-configured /proc mount. While this attack scenario is not
|
||||||
|
// common, in container runtimes it is possible for higher-level runtimes to be
|
||||||
|
// tricked into configuring an unsafe /proc that can be used to attack file
|
||||||
|
// operations. See [CVE-2019-19921] for more details.
|
||||||
|
//
|
||||||
|
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
||||||
|
func Reopen(handle *os.File, flags int) (*os.File, error) {
|
||||||
|
procRoot, err := getProcRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't operate on /proc/thread-self/fd/$n directly when doing a
|
||||||
|
// re-open, so we need to open /proc/thread-self/fd and then open a single
|
||||||
|
// final component.
|
||||||
|
procFdDir, closer, err := procThreadSelf(procRoot, "fd/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
|
||||||
|
}
|
||||||
|
defer procFdDir.Close()
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
// Try to detect if there is a mount on top of the magic-link we are about
|
||||||
|
// to open. If we are using unsafeHostProcRoot(), this could change after
|
||||||
|
// we check it (and there's nothing we can do about that) but for
|
||||||
|
// privateProcRoot() this should be guaranteed to be safe (at least since
|
||||||
|
// Linux 5.12[1], when anonymous mount namespaces were completely isolated
|
||||||
|
// from external mounts including mount propagation events).
|
||||||
|
//
|
||||||
|
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
||||||
|
// onto targets that reside on shared mounts").
|
||||||
|
fdStr := strconv.Itoa(int(handle.Fd()))
|
||||||
|
if err := checkSymlinkOvermount(procRoot, procFdDir, fdStr); err != nil {
|
||||||
|
return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
flags |= unix.O_CLOEXEC
|
||||||
|
// Rather than just wrapping openatFile, open-code it so we can copy
|
||||||
|
// handle.Name().
|
||||||
|
reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(reopenFd), handle.Name()), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hasOpenat2 = sync_OnceValue(func() bool {
|
||||||
|
fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = unix.Close(fd)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
|
||||||
|
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
|
||||||
|
// ".." while a mount or rename occurs anywhere on the system. This could
|
||||||
|
// happen spuriously, or as the result of an attacker trying to mess with
|
||||||
|
// us during lookup.
|
||||||
|
//
|
||||||
|
// In addition, scoped lookups have a "safety check" at the end of
|
||||||
|
// complete_walk which will return -EXDEV if the final path is not in the
|
||||||
|
// root.
|
||||||
|
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
|
||||||
|
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedLookupMaxRetries = 10
|
||||||
|
|
||||||
|
func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) {
|
||||||
|
fullPath := dir.Name() + "/" + path
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
how.Flags |= unix.O_CLOEXEC
|
||||||
|
var tries int
|
||||||
|
for tries < scopedLookupMaxRetries {
|
||||||
|
fd, err := unix.Openat2(int(dir.Fd()), path, how)
|
||||||
|
if err != nil {
|
||||||
|
if scopedLookupShouldRetry(how, err) {
|
||||||
|
// We retry a couple of times to avoid the spurious errors, and
|
||||||
|
// if we are being attacked then returning -EAGAIN is the best
|
||||||
|
// we can do.
|
||||||
|
tries++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
|
||||||
|
}
|
||||||
|
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
|
||||||
|
// NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise
|
||||||
|
// you'll get infinite recursion here.
|
||||||
|
if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
|
||||||
|
if actualPath, err := rawProcSelfFdReadlink(fd); err == nil {
|
||||||
|
fullPath = actualPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), fullPath), nil
|
||||||
|
}
|
||||||
|
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) {
|
||||||
|
if !partial {
|
||||||
|
file, err := openat2File(root, unsafePath, &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
||||||
|
})
|
||||||
|
return file, "", err
|
||||||
|
}
|
||||||
|
return partialLookupOpenat2(root, unsafePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialLookupOpenat2 is an alternative implementation of
|
||||||
|
// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
|
||||||
|
// handle to the deepest existing child of the requested path within the root.
|
||||||
|
func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, error) {
|
||||||
|
// TODO: Implement this as a git-bisect-like binary search.
|
||||||
|
|
||||||
|
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||||
|
endIdx := len(unsafePath)
|
||||||
|
var lastError error
|
||||||
|
for endIdx > 0 {
|
||||||
|
subpath := unsafePath[:endIdx]
|
||||||
|
|
||||||
|
handle, err := openat2File(root, subpath, &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
// Jump over the slash if we have a non-"" remainingPath.
|
||||||
|
if endIdx < len(unsafePath) {
|
||||||
|
endIdx += 1
|
||||||
|
}
|
||||||
|
// We found a subpath!
|
||||||
|
return handle, unsafePath[endIdx:], lastError
|
||||||
|
}
|
||||||
|
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
|
||||||
|
// That path doesn't exist, let's try the next directory up.
|
||||||
|
endIdx = strings.LastIndexByte(subpath, '/')
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, "", fmt.Errorf("open subpath: %w", err)
|
||||||
|
}
|
||||||
|
// If we couldn't open anything, the whole subpath is missing. Return a
|
||||||
|
// copy of the root fd so that the caller doesn't close this one by
|
||||||
|
// accident.
|
||||||
|
rootClone, err := dupFile(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return rootClone, unsafePath, lastError
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dupFile(f *os.File) (*os.File, error) {
|
||||||
|
fd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), f.Name()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openatFile(dir *os.File, path string, flags int, mode int) (*os.File, error) {
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.O_CLOEXEC
|
||||||
|
fd, err := unix.Openat(int(dir.Fd()), path, flags, uint32(mode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, &os.PathError{Op: "openat", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
// All of the paths we use with openatFile(2) are guaranteed to be
|
||||||
|
// lexically safe, so we can use path.Join here.
|
||||||
|
fullPath := filepath.Join(dir.Name(), path)
|
||||||
|
return os.NewFile(uintptr(fd), fullPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fstatatFile(dir *os.File, path string, flags int) (unix.Stat_t, error) {
|
||||||
|
var stat unix.Stat_t
|
||||||
|
if err := unix.Fstatat(int(dir.Fd()), path, &stat, flags); err != nil {
|
||||||
|
return stat, &os.PathError{Op: "fstatat", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
return stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readlinkatFile(dir *os.File, path string) (string, error) {
|
||||||
|
size := 4096
|
||||||
|
for {
|
||||||
|
linkBuf := make([]byte, size)
|
||||||
|
n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf)
|
||||||
|
if err != nil {
|
||||||
|
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
if n != size {
|
||||||
|
return string(linkBuf[:n]), nil
|
||||||
|
}
|
||||||
|
// Possible truncation, resize the buffer.
|
||||||
|
size *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,452 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fstat(f *os.File) (unix.Stat_t, error) {
|
||||||
|
var stat unix.Stat_t
|
||||||
|
if err := unix.Fstat(int(f.Fd()), &stat); err != nil {
|
||||||
|
return stat, &os.PathError{Op: "fstat", Path: f.Name(), Err: err}
|
||||||
|
}
|
||||||
|
return stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fstatfs(f *os.File) (unix.Statfs_t, error) {
|
||||||
|
var statfs unix.Statfs_t
|
||||||
|
if err := unix.Fstatfs(int(f.Fd()), &statfs); err != nil {
|
||||||
|
return statfs, &os.PathError{Op: "fstatfs", Path: f.Name(), Err: err}
|
||||||
|
}
|
||||||
|
return statfs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The kernel guarantees that the root inode of a procfs mount has an
|
||||||
|
// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO.
|
||||||
|
const (
|
||||||
|
procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC
|
||||||
|
procRootIno = 1 // PROC_ROOT_INO
|
||||||
|
)
|
||||||
|
|
||||||
|
func verifyProcRoot(procRoot *os.File) error {
|
||||||
|
if statfs, err := fstatfs(procRoot); err != nil {
|
||||||
|
return err
|
||||||
|
} else if statfs.Type != procSuperMagic {
|
||||||
|
return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type)
|
||||||
|
}
|
||||||
|
if stat, err := fstat(procRoot); err != nil {
|
||||||
|
return err
|
||||||
|
} else if stat.Ino != procRootIno {
|
||||||
|
return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNewMountApi = sync_OnceValue(func() bool {
|
||||||
|
// All of the pieces of the new mount API we use (fsopen, fsconfig,
|
||||||
|
// fsmount, open_tree) were added together in Linux 5.1[1,2], so we can
|
||||||
|
// just check for one of the syscalls and the others should also be
|
||||||
|
// available.
|
||||||
|
//
|
||||||
|
// Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE.
|
||||||
|
// This is equivalent to openat(2), but tells us if open_tree is
|
||||||
|
// available (and thus all of the other basic new mount API syscalls).
|
||||||
|
// open_tree(2) is most light-weight syscall to test here.
|
||||||
|
//
|
||||||
|
// [1]: merge commit 400913252d09
|
||||||
|
// [2]: <https://lore.kernel.org/lkml/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/>
|
||||||
|
fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = unix.Close(fd)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
func fsopen(fsName string, flags int) (*os.File, error) {
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.FSOPEN_CLOEXEC
|
||||||
|
fd, err := unix.Fsopen(fsName, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("fsopen "+fsName, err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fsmount(ctx *os.File, flags, mountAttrs int) (*os.File, error) {
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.FSMOUNT_CLOEXEC
|
||||||
|
fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("fsmount "+ctx.Name(), err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrivateProcMount() (*os.File, error) {
|
||||||
|
procfsCtx, err := fsopen("proc", unix.FSOPEN_CLOEXEC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer procfsCtx.Close()
|
||||||
|
|
||||||
|
// Try to configure hidepid=ptraceable,subset=pid if possible, but ignore errors.
|
||||||
|
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable")
|
||||||
|
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid")
|
||||||
|
|
||||||
|
// Get an actual handle.
|
||||||
|
if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil {
|
||||||
|
return nil, os.NewSyscallError("fsconfig create procfs", err)
|
||||||
|
}
|
||||||
|
return fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_RDONLY|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTree(dir *os.File, path string, flags uint) (*os.File, error) {
|
||||||
|
dirFd := -int(unix.EBADF)
|
||||||
|
dirName := "."
|
||||||
|
if dir != nil {
|
||||||
|
dirFd = int(dir.Fd())
|
||||||
|
dirName = dir.Name()
|
||||||
|
}
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.OPEN_TREE_CLOEXEC
|
||||||
|
fd, err := unix.OpenTree(dirFd, path, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &os.PathError{Op: "open_tree", Path: path, Err: err}
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), dirName+"/"+path), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clonePrivateProcMount() (_ *os.File, Err error) {
|
||||||
|
// Try to make a clone without using AT_RECURSIVE if we can. If this works,
|
||||||
|
// we can be sure there are no over-mounts and so if the root is valid then
|
||||||
|
// we're golden. Otherwise, we have to deal with over-mounts.
|
||||||
|
procfsHandle, err := openTree(nil, "/proc", unix.OPEN_TREE_CLONE)
|
||||||
|
if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procfsHandle) {
|
||||||
|
procfsHandle, err = openTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating a detached procfs clone: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = procfsHandle.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := verifyProcRoot(procfsHandle); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return procfsHandle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privateProcRoot() (*os.File, error) {
|
||||||
|
if !hasNewMountApi() || hookForceGetProcRootUnsafe() {
|
||||||
|
return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP)
|
||||||
|
}
|
||||||
|
// Try to create a new procfs mount from scratch if we can. This ensures we
|
||||||
|
// can get a procfs mount even if /proc is fake (for whatever reason).
|
||||||
|
procRoot, err := newPrivateProcMount()
|
||||||
|
if err != nil || hookForcePrivateProcRootOpenTree(procRoot) {
|
||||||
|
// Try to clone /proc then...
|
||||||
|
procRoot, err = clonePrivateProcMount()
|
||||||
|
}
|
||||||
|
return procRoot, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsafeHostProcRoot() (_ *os.File, Err error) {
|
||||||
|
procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = procRoot.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := verifyProcRoot(procRoot); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return procRoot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doGetProcRoot() (*os.File, error) {
|
||||||
|
procRoot, err := privateProcRoot()
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to using a /proc handle if making a private mount failed.
|
||||||
|
// If we have openat2, at least we can avoid some kinds of over-mount
|
||||||
|
// attacks, but without openat2 there's not much we can do.
|
||||||
|
procRoot, err = unsafeHostProcRoot()
|
||||||
|
}
|
||||||
|
return procRoot, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var getProcRoot = sync_OnceValues(func() (*os.File, error) {
|
||||||
|
return doGetProcRoot()
|
||||||
|
})
|
||||||
|
|
||||||
|
var hasProcThreadSelf = sync_OnceValue(func() bool {
|
||||||
|
return unix.Access("/proc/thread-self/", unix.F_OK) == nil
|
||||||
|
})
|
||||||
|
|
||||||
|
var errUnsafeProcfs = errors.New("unsafe procfs detected")
|
||||||
|
|
||||||
|
type procThreadSelfCloser func()
|
||||||
|
|
||||||
|
// procThreadSelf returns a handle to /proc/thread-self/<subpath> (or an
|
||||||
|
// equivalent handle on older kernels where /proc/thread-self doesn't exist).
|
||||||
|
// Once finished with the handle, you must call the returned closer function
|
||||||
|
// (runtime.UnlockOSThread). You must not pass the returned *os.File to other
|
||||||
|
// Go threads or use the handle after calling the closer.
|
||||||
|
//
|
||||||
|
// This is similar to ProcThreadSelf from runc, but with extra hardening
|
||||||
|
// applied and using *os.File.
|
||||||
|
func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) {
|
||||||
|
// We need to lock our thread until the caller is done with the handle
|
||||||
|
// because between getting the handle and using it we could get interrupted
|
||||||
|
// by the Go runtime and hit the case where the underlying thread is
|
||||||
|
// swapped out and the original thread is killed, resulting in
|
||||||
|
// pull-your-hair-out-hard-to-debug issues in the caller.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
runtime.UnlockOSThread()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Figure out what prefix we want to use.
|
||||||
|
threadSelf := "thread-self/"
|
||||||
|
if !hasProcThreadSelf() || hookForceProcSelfTask() {
|
||||||
|
/// Pre-3.17 kernels don't have /proc/thread-self, so do it manually.
|
||||||
|
threadSelf = "self/task/" + strconv.Itoa(unix.Gettid()) + "/"
|
||||||
|
if _, err := fstatatFile(procRoot, threadSelf, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() {
|
||||||
|
// In this case, we running in a pid namespace that doesn't match
|
||||||
|
// the /proc mount we have. This can happen inside runc.
|
||||||
|
//
|
||||||
|
// Unfortunately, there is no nice way to get the correct TID to
|
||||||
|
// use here because of the age of the kernel, so we have to just
|
||||||
|
// use /proc/self and hope that it works.
|
||||||
|
threadSelf = "self/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the handle.
|
||||||
|
var (
|
||||||
|
handle *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if hasOpenat2() {
|
||||||
|
// We prefer being able to use RESOLVE_NO_XDEV if we can, to be
|
||||||
|
// absolutely sure we are operating on a clean /proc handle that
|
||||||
|
// doesn't have any cheeky overmounts that could trick us (including
|
||||||
|
// symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't
|
||||||
|
// strictly needed, but just use it since we have it.
|
||||||
|
//
|
||||||
|
// NOTE: /proc/self is technically a magic-link (the contents of the
|
||||||
|
// symlink are generated dynamically), but it doesn't use
|
||||||
|
// nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it.
|
||||||
|
//
|
||||||
|
// NOTE: We MUST NOT use RESOLVE_IN_ROOT here, as openat2File uses
|
||||||
|
// procSelfFdReadlink to clean up the returned f.Name() if we use
|
||||||
|
// RESOLVE_IN_ROOT (which would lead to an infinite recursion).
|
||||||
|
handle, err = openat2File(procRoot, threadSelf+subpath, &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||||
|
// multiple %w verbs for this wrapping. For now we need to use a
|
||||||
|
// compatibility shim for older Go versions.
|
||||||
|
//err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
|
||||||
|
return nil, nil, wrapBaseError(err, errUnsafeProcfs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handle, err = openatFile(procRoot, threadSelf+subpath, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||||
|
// multiple %w verbs for this wrapping. For now we need to use a
|
||||||
|
// compatibility shim for older Go versions.
|
||||||
|
//err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
|
||||||
|
return nil, nil, wrapBaseError(err, errUnsafeProcfs)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = handle.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// We can't detect bind-mounts of different parts of procfs on top of
|
||||||
|
// /proc (a-la RESOLVE_NO_XDEV), but we can at least be sure that we
|
||||||
|
// aren't on the wrong filesystem here.
|
||||||
|
if statfs, err := fstatfs(handle); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if statfs.Type != procSuperMagic {
|
||||||
|
return nil, nil, fmt.Errorf("%w: incorrect /proc/self/fd filesystem type 0x%x", errUnsafeProcfs, statfs.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handle, runtime.UnlockOSThread, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to
|
||||||
|
// avoid bumping the requirement for a single constant we can just define it
|
||||||
|
// ourselves.
|
||||||
|
const STATX_MNT_ID_UNIQUE = 0x4000
|
||||||
|
|
||||||
|
var hasStatxMountId = sync_OnceValue(func() bool {
|
||||||
|
var (
|
||||||
|
stx unix.Statx_t
|
||||||
|
// We don't care which mount ID we get. The kernel will give us the
|
||||||
|
// unique one if it is supported.
|
||||||
|
wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
|
||||||
|
)
|
||||||
|
err := unix.Statx(-int(unix.EBADF), "/", 0, int(wantStxMask), &stx)
|
||||||
|
return err == nil && stx.Mask&wantStxMask != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
func getMountId(dir *os.File, path string) (uint64, error) {
|
||||||
|
// If we don't have statx(STATX_MNT_ID*) support, we can't do anything.
|
||||||
|
if !hasStatxMountId() {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
stx unix.Statx_t
|
||||||
|
// We don't care which mount ID we get. The kernel will give us the
|
||||||
|
// unique one if it is supported.
|
||||||
|
wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
err := unix.Statx(int(dir.Fd()), path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, int(wantStxMask), &stx)
|
||||||
|
if stx.Mask&wantStxMask == 0 {
|
||||||
|
// It's not a kernel limitation, for some reason we couldn't get a
|
||||||
|
// mount ID. Assume it's some kind of attack.
|
||||||
|
err = fmt.Errorf("%w: could not get mount id", errUnsafeProcfs)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
return stx.Mnt_id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSymlinkOvermount(procRoot *os.File, dir *os.File, path string) error {
|
||||||
|
// Get the mntId of our procfs handle.
|
||||||
|
expectedMountId, err := getMountId(procRoot, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Get the mntId of the target magic-link.
|
||||||
|
gotMountId, err := getMountId(dir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// As long as the directory mount is alive, even with wrapping mount IDs,
|
||||||
|
// we would expect to see a different mount ID here. (Of course, if we're
|
||||||
|
// using unsafeHostProcRoot() then an attaker could change this after we
|
||||||
|
// did this check.)
|
||||||
|
if expectedMountId != gotMountId {
|
||||||
|
return fmt.Errorf("%w: symlink %s/%s has an overmount obscuring the real link (mount ids do not match %d != %d)", errUnsafeProcfs, dir.Name(), path, expectedMountId, gotMountId)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRawProcSelfFdReadlink(procRoot *os.File, fd int) (string, error) {
|
||||||
|
fdPath := fmt.Sprintf("fd/%d", fd)
|
||||||
|
procFdLink, closer, err := procThreadSelf(procRoot, fdPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get safe /proc/thread-self/%s handle: %w", fdPath, err)
|
||||||
|
}
|
||||||
|
defer procFdLink.Close()
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
// Try to detect if there is a mount on top of the magic-link. Since we use the handle directly
|
||||||
|
// provide to the closure. If the closure uses the handle directly, this
|
||||||
|
// should be safe in general (a mount on top of the path afterwards would
|
||||||
|
// not affect the handle itself) and will definitely be safe if we are
|
||||||
|
// using privateProcRoot() (at least since Linux 5.12[1], when anonymous
|
||||||
|
// mount namespaces were completely isolated from external mounts including
|
||||||
|
// mount propagation events).
|
||||||
|
//
|
||||||
|
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
||||||
|
// onto targets that reside on shared mounts").
|
||||||
|
if err := checkSymlinkOvermount(procRoot, procFdLink, ""); err != nil {
|
||||||
|
return "", fmt.Errorf("check safety of /proc/thread-self/fd/%d magiclink: %w", fd, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit
|
||||||
|
// 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty
|
||||||
|
// relative pathnames").
|
||||||
|
return readlinkatFile(procFdLink, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawProcSelfFdReadlink(fd int) (string, error) {
|
||||||
|
procRoot, err := getProcRoot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return doRawProcSelfFdReadlink(procRoot, fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func procSelfFdReadlink(f *os.File) (string, error) {
|
||||||
|
return rawProcSelfFdReadlink(int(f.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errPossibleBreakout = errors.New("possible breakout detected")
|
||||||
|
errInvalidDirectory = errors.New("wandered into deleted directory")
|
||||||
|
errDeletedInode = errors.New("cannot verify path of deleted inode")
|
||||||
|
)
|
||||||
|
|
||||||
|
func isDeadInode(file *os.File) error {
|
||||||
|
// If the nlink of a file drops to 0, there is an attacker deleting
|
||||||
|
// directories during our walk, which could result in weird /proc values.
|
||||||
|
// It's better to error out in this case.
|
||||||
|
stat, err := fstat(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check for dead inode: %w", err)
|
||||||
|
}
|
||||||
|
if stat.Nlink == 0 {
|
||||||
|
err := errDeletedInode
|
||||||
|
if stat.Mode&unix.S_IFMT == unix.S_IFDIR {
|
||||||
|
err = errInvalidDirectory
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w %q", err, file.Name())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkProcSelfFdPath(path string, file *os.File) error {
|
||||||
|
if err := isDeadInode(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
actualPath, err := procSelfFdReadlink(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get path of handle: %w", err)
|
||||||
|
}
|
||||||
|
if actualPath != path {
|
||||||
|
return fmt.Errorf("%w: handle path %q doesn't match expected path %q", errPossibleBreakout, actualPath, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test hooks used in the procfs tests to verify that the fallback logic works.
|
||||||
|
// See testing_mocks_linux_test.go and procfs_linux_test.go for more details.
|
||||||
|
var (
|
||||||
|
hookForcePrivateProcRootOpenTree = hookDummyFile
|
||||||
|
hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile
|
||||||
|
hookForceGetProcRootUnsafe = hookDummy
|
||||||
|
|
||||||
|
hookForceProcSelfTask = hookDummy
|
||||||
|
hookForceProcSelf = hookDummy
|
||||||
|
)
|
||||||
|
|
||||||
|
func hookDummy() bool { return false }
|
||||||
|
func hookDummyFile(_ *os.File) bool { return false }
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (C) 2017 SUSE LLC. All rights reserved.
|
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
|
@ -10,19 +10,19 @@ import "os"
|
||||||
// are several projects (umoci and go-mtree) that are using this sort of
|
// are several projects (umoci and go-mtree) that are using this sort of
|
||||||
// interface.
|
// interface.
|
||||||
|
|
||||||
// VFS is the minimal interface necessary to use SecureJoinVFS. A nil VFS is
|
// VFS is the minimal interface necessary to use [SecureJoinVFS]. A nil VFS is
|
||||||
// equivalent to using the standard os.* family of functions. This is mainly
|
// equivalent to using the standard [os].* family of functions. This is mainly
|
||||||
// used for the purposes of mock testing, but also can be used to otherwise use
|
// used for the purposes of mock testing, but also can be used to otherwise use
|
||||||
// SecureJoin with VFS-like system.
|
// [SecureJoinVFS] with VFS-like system.
|
||||||
type VFS interface {
|
type VFS interface {
|
||||||
// Lstat returns a FileInfo describing the named file. If the file is a
|
// Lstat returns an [os.FileInfo] describing the named file. If the
|
||||||
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
|
// file is a symbolic link, the returned [os.FileInfo] describes the
|
||||||
// makes no attempt to follow the link. These semantics are identical to
|
// symbolic link. Lstat makes no attempt to follow the link.
|
||||||
// os.Lstat.
|
// The semantics are identical to [os.Lstat].
|
||||||
Lstat(name string) (os.FileInfo, error)
|
Lstat(name string) (os.FileInfo, error)
|
||||||
|
|
||||||
// Readlink returns the destination of the named symbolic link. These
|
// Readlink returns the destination of the named symbolic link.
|
||||||
// semantics are identical to os.Readlink.
|
// The semantics are identical to [os.Readlink].
|
||||||
Readlink(name string) (string, error)
|
Readlink(name string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,12 +30,6 @@ type VFS interface {
|
||||||
// module.
|
// module.
|
||||||
type osVFS struct{}
|
type osVFS struct{}
|
||||||
|
|
||||||
// Lstat returns a FileInfo describing the named file. If the file is a
|
|
||||||
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
|
|
||||||
// makes no attempt to follow the link. These semantics are identical to
|
|
||||||
// os.Lstat.
|
|
||||||
func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) }
|
func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) }
|
||||||
|
|
||||||
// Readlink returns the destination of the named symbolic link. These
|
|
||||||
// semantics are identical to os.Readlink.
|
|
||||||
func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) }
|
func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Go parameters
|
# Go parameters
|
||||||
GOCMD = go
|
GOCMD = go
|
||||||
GOTEST = $(GOCMD) test
|
GOTEST = $(GOCMD) test
|
||||||
|
WASIRUN_WRAPPER := $(CURDIR)/scripts/wasirun-wrapper
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
|
|
@ -9,3 +10,9 @@ test:
|
||||||
test-coverage:
|
test-coverage:
|
||||||
echo "" > $(COVERAGE_REPORT); \
|
echo "" > $(COVERAGE_REPORT); \
|
||||||
$(GOTEST) -coverprofile=$(COVERAGE_REPORT) -coverpkg=./... -covermode=$(COVERAGE_MODE) ./...
|
$(GOTEST) -coverprofile=$(COVERAGE_REPORT) -coverpkg=./... -covermode=$(COVERAGE_MODE) ./...
|
||||||
|
|
||||||
|
.PHONY: wasitest
|
||||||
|
wasitest: export GOARCH=wasm
|
||||||
|
wasitest: export GOOS=wasip1
|
||||||
|
wasitest:
|
||||||
|
$(GOTEST) -exec $(WASIRUN_WRAPPER) ./...
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,8 @@ type File interface {
|
||||||
// Name returns the name of the file as presented to Open.
|
// Name returns the name of the file as presented to Open.
|
||||||
Name() string
|
Name() string
|
||||||
io.Writer
|
io.Writer
|
||||||
|
// TODO: Add io.WriterAt for v6
|
||||||
|
// io.WriterAt
|
||||||
io.Reader
|
io.Reader
|
||||||
io.ReaderAt
|
io.ReaderAt
|
||||||
io.Seeker
|
io.Seeker
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-billy/v5"
|
"github.com/go-git/go-billy/v5"
|
||||||
|
|
@ -18,16 +19,19 @@ import (
|
||||||
|
|
||||||
const separator = filepath.Separator
|
const separator = filepath.Separator
|
||||||
|
|
||||||
// Memory a very convenient filesystem based on memory files
|
var errNotLink = errors.New("not a link")
|
||||||
|
|
||||||
|
// Memory a very convenient filesystem based on memory files.
|
||||||
type Memory struct {
|
type Memory struct {
|
||||||
s *storage
|
s *storage
|
||||||
|
|
||||||
tempCount int
|
tempCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
//New returns a new Memory filesystem.
|
// New returns a new Memory filesystem.
|
||||||
func New() billy.Filesystem {
|
func New() billy.Filesystem {
|
||||||
fs := &Memory{s: newStorage()}
|
fs := &Memory{s: newStorage()}
|
||||||
|
fs.s.New("/", 0755|os.ModeDir, 0)
|
||||||
return chroot.New(fs, string(separator))
|
return chroot.New(fs, string(separator))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +61,9 @@ func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.F
|
||||||
}
|
}
|
||||||
|
|
||||||
if target, isLink := fs.resolveLink(filename, f); isLink {
|
if target, isLink := fs.resolveLink(filename, f); isLink {
|
||||||
return fs.OpenFile(target, flag, perm)
|
if target != filename {
|
||||||
|
return fs.OpenFile(target, flag, perm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,8 +74,6 @@ func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.F
|
||||||
return f.Duplicate(filename, perm, flag), nil
|
return f.Duplicate(filename, perm, flag), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var errNotLink = errors.New("not a link")
|
|
||||||
|
|
||||||
func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
|
func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
|
||||||
if !isSymlink(f.mode) {
|
if !isSymlink(f.mode) {
|
||||||
return fullpath, false
|
return fullpath, false
|
||||||
|
|
@ -131,8 +135,12 @@ func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
|
func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
|
||||||
if f, has := fs.s.Get(path); has {
|
if f, has := fs.s.Get(path); has {
|
||||||
if target, isLink := fs.resolveLink(path, f); isLink {
|
if target, isLink := fs.resolveLink(path, f); isLink {
|
||||||
return fs.ReadDir(target)
|
if target != path {
|
||||||
|
return fs.ReadDir(target)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries []os.FileInfo
|
var entries []os.FileInfo
|
||||||
|
|
@ -169,17 +177,19 @@ func (fs *Memory) Remove(filename string) error {
|
||||||
return fs.s.Remove(filename)
|
return fs.s.Remove(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Falls back to Go's filepath.Join, which works differently depending on the
|
||||||
|
// OS where the code is being executed.
|
||||||
func (fs *Memory) Join(elem ...string) string {
|
func (fs *Memory) Join(elem ...string) string {
|
||||||
return filepath.Join(elem...)
|
return filepath.Join(elem...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *Memory) Symlink(target, link string) error {
|
func (fs *Memory) Symlink(target, link string) error {
|
||||||
_, err := fs.Stat(link)
|
_, err := fs.Lstat(link)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return os.ErrExist
|
return os.ErrExist
|
||||||
}
|
}
|
||||||
|
|
||||||
if !os.IsNotExist(err) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +240,7 @@ func (f *file) Read(b []byte) (int, error) {
|
||||||
n, err := f.ReadAt(b, f.position)
|
n, err := f.ReadAt(b, f.position)
|
||||||
f.position += int64(n)
|
f.position += int64(n)
|
||||||
|
|
||||||
if err == io.EOF && n != 0 {
|
if errors.Is(err, io.EOF) && n != 0 {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,6 +279,10 @@ func (f *file) Seek(offset int64, whence int) (int64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *file) Write(p []byte) (int, error) {
|
func (f *file) Write(p []byte) (int, error) {
|
||||||
|
return f.WriteAt(p, f.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *file) WriteAt(p []byte, off int64) (int, error) {
|
||||||
if f.isClosed {
|
if f.isClosed {
|
||||||
return 0, os.ErrClosed
|
return 0, os.ErrClosed
|
||||||
}
|
}
|
||||||
|
|
@ -277,8 +291,8 @@ func (f *file) Write(p []byte) (int, error) {
|
||||||
return 0, errors.New("write not supported")
|
return 0, errors.New("write not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := f.content.WriteAt(p, f.position)
|
n, err := f.content.WriteAt(p, off)
|
||||||
f.position += int64(n)
|
f.position = off + int64(n)
|
||||||
|
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ func (s *storage) Rename(from, to string) error {
|
||||||
move := [][2]string{{from, to}}
|
move := [][2]string{{from, to}}
|
||||||
|
|
||||||
for pathFrom := range s.files {
|
for pathFrom := range s.files {
|
||||||
if pathFrom == from || !filepath.HasPrefix(pathFrom, from) {
|
if pathFrom == from || !strings.HasPrefix(pathFrom, from) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,10 @@ func (fs *BoundOS) insideBaseDir(filename string) (bool, error) {
|
||||||
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
||||||
// that either filename or fs.baseDir may contain.
|
// that either filename or fs.baseDir may contain.
|
||||||
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
||||||
|
// "/" contains all others.
|
||||||
|
if fs.baseDir == "/" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||||||
if dir == "" || os.IsNotExist(err) {
|
if dir == "" || os.IsNotExist(err) {
|
||||||
dir = filepath.Dir(filename)
|
dir = filepath.Dir(filename)
|
||||||
|
|
@ -255,7 +259,7 @@ func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
||||||
wd = fs.baseDir
|
wd = fs.baseDir
|
||||||
}
|
}
|
||||||
if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
|
if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
|
||||||
return false, fmt.Errorf("path outside base dir")
|
return false, fmt.Errorf("%q: path outside base dir %q: %w", filename, fs.baseDir, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//go:build !plan9 && !windows && !js
|
//go:build !plan9 && !windows && !wasm
|
||||||
// +build !plan9,!windows,!js
|
// +build !plan9,!windows,!wasm
|
||||||
|
|
||||||
package osfs
|
package osfs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
//go:build wasip1
|
||||||
|
// +build wasip1
|
||||||
|
|
||||||
|
package osfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *file) Lock() error {
|
||||||
|
f.m.Lock()
|
||||||
|
defer f.m.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *file) Unlock() error {
|
||||||
|
f.m.Lock()
|
||||||
|
defer f.m.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rename(from, to string) error {
|
||||||
|
return os.Rename(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// umask sets umask to a new value, and returns a func which allows the
|
||||||
|
// caller to reset it back to what it was originally.
|
||||||
|
func umask(new int) func() {
|
||||||
|
old := syscall.Umask(new)
|
||||||
|
return func() {
|
||||||
|
syscall.Umask(old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -33,14 +34,14 @@ func removeAll(fs billy.Basic, path string) error {
|
||||||
|
|
||||||
// Simple case: if Remove works, we're done.
|
// Simple case: if Remove works, we're done.
|
||||||
err := fs.Remove(path)
|
err := fs.Remove(path)
|
||||||
if err == nil || os.IsNotExist(err) {
|
if err == nil || errors.Is(err, os.ErrNotExist) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, is this a directory we need to recurse into?
|
// Otherwise, is this a directory we need to recurse into?
|
||||||
dir, serr := fs.Stat(path)
|
dir, serr := fs.Stat(path)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
if os.IsNotExist(serr) {
|
if errors.Is(serr, os.ErrNotExist) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||||
// Directory.
|
// Directory.
|
||||||
fis, err := dirfs.ReadDir(path)
|
fis, err := dirfs.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
// Race. It was deleted between the Lstat and Open.
|
// Race. It was deleted between the Lstat and Open.
|
||||||
// Return nil per RemoveAll's docs.
|
// Return nil per RemoveAll's docs.
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -81,7 +82,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||||
|
|
||||||
// Remove directory.
|
// Remove directory.
|
||||||
err1 := fs.Remove(path)
|
err1 := fs.Remove(path)
|
||||||
if err1 == nil || os.IsNotExist(err1) {
|
if err1 == nil || errors.Is(err1, os.ErrNotExist) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,22 +97,26 @@ func removeAll(fs billy.Basic, path string) error {
|
||||||
// WriteFile writes data to a file named by filename in the given filesystem.
|
// WriteFile writes data to a file named by filename in the given filesystem.
|
||||||
// If the file does not exist, WriteFile creates it with permissions perm;
|
// If the file does not exist, WriteFile creates it with permissions perm;
|
||||||
// otherwise WriteFile truncates it before writing.
|
// otherwise WriteFile truncates it before writing.
|
||||||
func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) error {
|
func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) (err error) {
|
||||||
f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if f != nil {
|
||||||
|
err1 := f.Close()
|
||||||
|
if err == nil {
|
||||||
|
err = err1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
n, err := f.Write(data)
|
n, err := f.Write(data)
|
||||||
if err == nil && n < len(data) {
|
if err == nil && n < len(data) {
|
||||||
err = io.ErrShortWrite
|
err = io.ErrShortWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
if err1 := f.Close(); err == nil {
|
return nil
|
||||||
err = err1
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random number state.
|
// Random number state.
|
||||||
|
|
@ -154,7 +159,7 @@ func TempFile(fs billy.Basic, dir, prefix string) (f billy.File, err error) {
|
||||||
for i := 0; i < 10000; i++ {
|
for i := 0; i < 10000; i++ {
|
||||||
name := filepath.Join(dir, prefix+nextSuffix())
|
name := filepath.Join(dir, prefix+nextSuffix())
|
||||||
f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||||
if os.IsExist(err) {
|
if errors.Is(err, os.ErrExist) {
|
||||||
if nconflict++; nconflict > 10 {
|
if nconflict++; nconflict > 10 {
|
||||||
randmu.Lock()
|
randmu.Lock()
|
||||||
rand = reseed()
|
rand = reseed()
|
||||||
|
|
@ -185,7 +190,7 @@ func TempDir(fs billy.Dir, dir, prefix string) (name string, err error) {
|
||||||
for i := 0; i < 10000; i++ {
|
for i := 0; i < 10000; i++ {
|
||||||
try := filepath.Join(dir, prefix+nextSuffix())
|
try := filepath.Join(dir, prefix+nextSuffix())
|
||||||
err = fs.MkdirAll(try, 0700)
|
err = fs.MkdirAll(try, 0700)
|
||||||
if os.IsExist(err) {
|
if errors.Is(err, os.ErrExist) {
|
||||||
if nconflict++; nconflict > 10 {
|
if nconflict++; nconflict > 10 {
|
||||||
randmu.Lock()
|
randmu.Lock()
|
||||||
rand = reseed()
|
rand = reseed()
|
||||||
|
|
@ -193,8 +198,8 @@ func TempDir(fs billy.Dir, dir, prefix string) (name string, err error) {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if os.IsNotExist(err) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -272,7 +277,7 @@ func ReadFile(fs billy.Basic, name string) ([]byte, error) {
|
||||||
data = data[:len(data)+n]
|
data = data[:len(data)+n]
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if errors.Is(err, io.EOF) {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ compatibility status with go-git.
|
||||||
| `init` | `--bare` | ✅ | | |
|
| `init` | `--bare` | ✅ | | |
|
||||||
| `init` | `--template` <br/> `--separate-git-dir` <br/> `--shared` | ❌ | | |
|
| `init` | `--template` <br/> `--separate-git-dir` <br/> `--shared` | ❌ | | |
|
||||||
| `clone` | | ✅ | | - [PlainClone](_examples/clone/main.go) |
|
| `clone` | | ✅ | | - [PlainClone](_examples/clone/main.go) |
|
||||||
| `clone` | Authentication: <br/> - none <br/> - access token <br/> - username + password <br/> - ssh | ✅ | | - [clone ssh](_examples/clone/auth/ssh/main.go) <br/> - [clone access token](_examples/clone/auth/basic/access_token/main.go) <br/> - [clone user + password](_examples/clone/auth/basic/username_password/main.go) |
|
| `clone` | Authentication: <br/> - none <br/> - access token <br/> - username + password <br/> - ssh | ✅ | | - [clone ssh (private_key)](_examples/clone/auth/ssh/private_key/main.go) <br/> - [clone ssh (ssh_agent)](_examples/clone/auth/ssh/ssh_agent/main.go) <br/> - [clone access token](_examples/clone/auth/basic/access_token/main.go) <br/> - [clone user + password](_examples/clone/auth/basic/username_password/main.go) |
|
||||||
| `clone` | `--progress` <br/> `--single-branch` <br/> `--depth` <br/> `--origin` <br/> `--recurse-submodules` <br/>`--shared` | ✅ | | - [recurse submodules](_examples/clone/main.go) <br/> - [progress](_examples/progress/main.go) |
|
| `clone` | `--progress` <br/> `--single-branch` <br/> `--depth` <br/> `--origin` <br/> `--recurse-submodules` <br/>`--shared` | ✅ | | - [recurse submodules](_examples/clone/main.go) <br/> - [progress](_examples/progress/main.go) |
|
||||||
|
|
||||||
## Basic snapshotting
|
## Basic snapshotting
|
||||||
|
|
@ -34,6 +34,7 @@ compatibility status with go-git.
|
||||||
| `merge` | | ⚠️ (partial) | Fast-forward only | |
|
| `merge` | | ⚠️ (partial) | Fast-forward only | |
|
||||||
| `mergetool` | | ❌ | | |
|
| `mergetool` | | ❌ | | |
|
||||||
| `stash` | | ❌ | | |
|
| `stash` | | ❌ | | |
|
||||||
|
| `sparse-checkout` | | ✅ | | - [sparse-checkout](_examples/sparse-checkout/main.go) |
|
||||||
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
|
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
|
||||||
|
|
||||||
## Sharing and updating projects
|
## Sharing and updating projects
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ In order for a PR to be accepted it needs to pass a list of requirements:
|
||||||
- If the PR is a new feature, it has to come with a suite of unit tests, that tests the new functionality.
|
- If the PR is a new feature, it has to come with a suite of unit tests, that tests the new functionality.
|
||||||
- In any case, all the PRs have to pass the personal evaluation of at least one of the maintainers of go-git.
|
- In any case, all the PRs have to pass the personal evaluation of at least one of the maintainers of go-git.
|
||||||
|
|
||||||
|
### Branches
|
||||||
|
|
||||||
|
The `master` branch is currently used for maintaining the `v5` major release only. The accepted changes would
|
||||||
|
be dependency bumps, bug fixes and small changes that aren't needed for `v6`. New development should target the
|
||||||
|
`v6-exp` branch, and if agreed with at least one go-git maintainer, it can be back ported to `v5` by creating
|
||||||
|
a new PR that targets `master`.
|
||||||
|
|
||||||
### Format of the commit message
|
### Format of the commit message
|
||||||
|
|
||||||
Every commit message should describe what was changed, under which context and, if applicable, the GitHub issue it relates to:
|
Every commit message should describe what was changed, under which context and, if applicable, the GitHub issue it relates to:
|
||||||
|
|
|
||||||
|
|
@ -97,13 +97,10 @@ func Blame(c *object.Commit, path string) (*BlameResult, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if finished == true {
|
if finished {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.lineToCommit = make([]*object.Commit, finalLength)
|
b.lineToCommit = make([]*object.Commit, finalLength)
|
||||||
for i := range needsMap {
|
for i := range needsMap {
|
||||||
|
|
@ -309,8 +306,8 @@ func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
|
||||||
for h := range hunks {
|
for h := range hunks {
|
||||||
hLines := countLines(hunks[h].Text)
|
hLines := countLines(hunks[h].Text)
|
||||||
for hl := 0; hl < hLines; hl++ {
|
for hl := 0; hl < hLines; hl++ {
|
||||||
switch {
|
switch hunks[h].Type {
|
||||||
case hunks[h].Type == diffmatchpatch.DiffEqual:
|
case diffmatchpatch.DiffEqual:
|
||||||
prevl++
|
prevl++
|
||||||
curl++
|
curl++
|
||||||
if curl == curItem.NeedsMap[need].Cur {
|
if curl == curItem.NeedsMap[need].Cur {
|
||||||
|
|
@ -322,7 +319,7 @@ func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case hunks[h].Type == diffmatchpatch.DiffInsert:
|
case diffmatchpatch.DiffInsert:
|
||||||
curl++
|
curl++
|
||||||
if curl == curItem.NeedsMap[need].Cur {
|
if curl == curItem.NeedsMap[need].Cur {
|
||||||
// the line we want is added, it may have been added here (or by another parent), skip it for now
|
// the line we want is added, it may have been added here (or by another parent), skip it for now
|
||||||
|
|
@ -331,7 +328,7 @@ func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case hunks[h].Type == diffmatchpatch.DiffDelete:
|
case diffmatchpatch.DiffDelete:
|
||||||
prevl += hLines
|
prevl += hLines
|
||||||
continue out
|
continue out
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ const (
|
||||||
extensionsSection = "extensions"
|
extensionsSection = "extensions"
|
||||||
fetchKey = "fetch"
|
fetchKey = "fetch"
|
||||||
urlKey = "url"
|
urlKey = "url"
|
||||||
|
pushurlKey = "pushurl"
|
||||||
bareKey = "bare"
|
bareKey = "bare"
|
||||||
worktreeKey = "worktree"
|
worktreeKey = "worktree"
|
||||||
commentCharKey = "commentChar"
|
commentCharKey = "commentChar"
|
||||||
|
|
@ -633,6 +634,7 @@ func (c *RemoteConfig) unmarshal(s *format.Subsection) error {
|
||||||
|
|
||||||
c.Name = c.raw.Name
|
c.Name = c.raw.Name
|
||||||
c.URLs = append([]string(nil), c.raw.Options.GetAll(urlKey)...)
|
c.URLs = append([]string(nil), c.raw.Options.GetAll(urlKey)...)
|
||||||
|
c.URLs = append(c.URLs, c.raw.Options.GetAll(pushurlKey)...)
|
||||||
c.Fetch = fetch
|
c.Fetch = fetch
|
||||||
c.Mirror = c.raw.Options.Get(mirrorKey) == "true"
|
c.Mirror = c.raw.Options.Get(mirrorKey) == "true"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ func tokenizeExpression(ch rune, tokenType token, check runeCategoryValidator, r
|
||||||
return tokenType, string(data), nil
|
return tokenType, string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxRevisionLength holds the maximum length that will be parsed for a
|
||||||
|
// revision. Git itself doesn't enforce a max length, but rather leans on
|
||||||
|
// the OS to enforce it via its ARG_MAX.
|
||||||
|
const maxRevisionLength = 128 * 1024 // 128kb
|
||||||
|
|
||||||
var zeroRune = rune(0)
|
var zeroRune = rune(0)
|
||||||
|
|
||||||
// scanner represents a lexical scanner.
|
// scanner represents a lexical scanner.
|
||||||
|
|
@ -52,7 +57,7 @@ type scanner struct {
|
||||||
|
|
||||||
// newScanner returns a new instance of scanner.
|
// newScanner returns a new instance of scanner.
|
||||||
func newScanner(r io.Reader) *scanner {
|
func newScanner(r io.Reader) *scanner {
|
||||||
return &scanner{r: bufio.NewReader(r)}
|
return &scanner{r: bufio.NewReader(io.LimitReader(r, maxRevisionLength))}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan extracts tokens and their strings counterpart
|
// Scan extracts tokens and their strings counterpart
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,9 @@ type ResetOptions struct {
|
||||||
// the index (resetting it to the tree of Commit) and the working tree
|
// the index (resetting it to the tree of Commit) and the working tree
|
||||||
// depending on Mode. If empty MixedReset is used.
|
// depending on Mode. If empty MixedReset is used.
|
||||||
Mode ResetMode
|
Mode ResetMode
|
||||||
|
// Files, if not empty will constrain the reseting the index to only files
|
||||||
|
// specified in this list.
|
||||||
|
Files []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields and sets the default values.
|
// Validate validates the fields and sets the default values.
|
||||||
|
|
@ -790,3 +793,26 @@ type PlainInitOptions struct {
|
||||||
|
|
||||||
// Validate validates the fields and sets the default values.
|
// Validate validates the fields and sets the default values.
|
||||||
func (o *PlainInitOptions) Validate() error { return nil }
|
func (o *PlainInitOptions) Validate() error { return nil }
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoRestorePaths = errors.New("you must specify path(s) to restore")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RestoreOptions describes how a restore should be performed.
|
||||||
|
type RestoreOptions struct {
|
||||||
|
// Marks to restore the content in the index
|
||||||
|
Staged bool
|
||||||
|
// Marks to restore the content of the working tree
|
||||||
|
Worktree bool
|
||||||
|
// List of file paths that will be restored
|
||||||
|
Files []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the fields and sets the default values.
|
||||||
|
func (o *RestoreOptions) Validate() error {
|
||||||
|
if len(o.Files) == 0 {
|
||||||
|
return ErrNoRestorePaths
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,10 @@ func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error)
|
||||||
|
|
||||||
for _, fi := range fis {
|
for _, fi := range fis {
|
||||||
if fi.IsDir() && fi.Name() != gitDir {
|
if fi.IsDir() && fi.Name() != gitDir {
|
||||||
|
if NewMatcher(ps).Match(append(path, fi.Name()), true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var subps []Pattern
|
var subps []Pattern
|
||||||
subps, err = ReadPatterns(fs, append(path, fi.Name()))
|
subps, err = ReadPatterns(fs, append(path, fi.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ var (
|
||||||
// ErrInvalidChecksum is returned by Decode if the SHA1 hash mismatch with
|
// ErrInvalidChecksum is returned by Decode if the SHA1 hash mismatch with
|
||||||
// the read content
|
// the read content
|
||||||
ErrInvalidChecksum = errors.New("invalid checksum")
|
ErrInvalidChecksum = errors.New("invalid checksum")
|
||||||
|
// ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
|
||||||
errUnknownExtension = errors.New("unknown extension")
|
ErrUnknownExtension = errors.New("unknown extension")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -39,6 +39,7 @@ const (
|
||||||
|
|
||||||
// A Decoder reads and decodes index files from an input stream.
|
// A Decoder reads and decodes index files from an input stream.
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
|
buf *bufio.Reader
|
||||||
r io.Reader
|
r io.Reader
|
||||||
hash hash.Hash
|
hash hash.Hash
|
||||||
lastEntry *Entry
|
lastEntry *Entry
|
||||||
|
|
@ -49,8 +50,10 @@ type Decoder struct {
|
||||||
// NewDecoder returns a new decoder that reads from r.
|
// NewDecoder returns a new decoder that reads from r.
|
||||||
func NewDecoder(r io.Reader) *Decoder {
|
func NewDecoder(r io.Reader) *Decoder {
|
||||||
h := hash.New(hash.CryptoType)
|
h := hash.New(hash.CryptoType)
|
||||||
|
buf := bufio.NewReader(r)
|
||||||
return &Decoder{
|
return &Decoder{
|
||||||
r: io.TeeReader(r, h),
|
buf: buf,
|
||||||
|
r: io.TeeReader(buf, h),
|
||||||
hash: h,
|
hash: h,
|
||||||
extReader: bufio.NewReader(nil),
|
extReader: bufio.NewReader(nil),
|
||||||
}
|
}
|
||||||
|
|
@ -210,71 +213,75 @@ func (d *Decoder) readExtensions(idx *Index) error {
|
||||||
// count that they are not supported by jgit or libgit
|
// count that they are not supported by jgit or libgit
|
||||||
|
|
||||||
var expected []byte
|
var expected []byte
|
||||||
|
var peeked []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var header [4]byte
|
// we should always be able to peek for 4 bytes (header) + 4 bytes (extlen) + final hash
|
||||||
|
// if this fails, we know that we're at the end of the index
|
||||||
|
peekLen := 4 + 4 + d.hash.Size()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
expected = d.hash.Sum(nil)
|
expected = d.hash.Sum(nil)
|
||||||
|
peeked, err = d.buf.Peek(peekLen)
|
||||||
var n int
|
if len(peeked) < peekLen {
|
||||||
if n, err = io.ReadFull(d.r, header[:]); err != nil {
|
// there can't be an extension at this point, so let's bail out
|
||||||
if n == 0 {
|
|
||||||
err = io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.readExtension(idx, header[:])
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != errUnknownExtension {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.readChecksum(expected, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Decoder) readExtension(idx *Index, header []byte) error {
|
|
||||||
switch {
|
|
||||||
case bytes.Equal(header, treeExtSignature):
|
|
||||||
r, err := d.getExtensionReader()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = d.readExtension(idx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.readChecksum(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) readExtension(idx *Index) error {
|
||||||
|
var header [4]byte
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(d.r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := d.getExtensionReader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(header[:], treeExtSignature):
|
||||||
idx.Cache = &Tree{}
|
idx.Cache = &Tree{}
|
||||||
d := &treeExtensionDecoder{r}
|
d := &treeExtensionDecoder{r}
|
||||||
if err := d.Decode(idx.Cache); err != nil {
|
if err := d.Decode(idx.Cache); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case bytes.Equal(header, resolveUndoExtSignature):
|
case bytes.Equal(header[:], resolveUndoExtSignature):
|
||||||
r, err := d.getExtensionReader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
idx.ResolveUndo = &ResolveUndo{}
|
idx.ResolveUndo = &ResolveUndo{}
|
||||||
d := &resolveUndoDecoder{r}
|
d := &resolveUndoDecoder{r}
|
||||||
if err := d.Decode(idx.ResolveUndo); err != nil {
|
if err := d.Decode(idx.ResolveUndo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case bytes.Equal(header, endOfIndexEntryExtSignature):
|
case bytes.Equal(header[:], endOfIndexEntryExtSignature):
|
||||||
r, err := d.getExtensionReader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
idx.EndOfIndexEntry = &EndOfIndexEntry{}
|
idx.EndOfIndexEntry = &EndOfIndexEntry{}
|
||||||
d := &endOfIndexEntryDecoder{r}
|
d := &endOfIndexEntryDecoder{r}
|
||||||
if err := d.Decode(idx.EndOfIndexEntry); err != nil {
|
if err := d.Decode(idx.EndOfIndexEntry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return errUnknownExtension
|
// See https://git-scm.com/docs/index-format, which says:
|
||||||
|
// If the first byte is 'A'..'Z' the extension is optional and can be ignored.
|
||||||
|
if header[0] < 'A' || header[0] > 'Z' {
|
||||||
|
return ErrUnknownExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &unknownExtensionDecoder{r}
|
||||||
|
if err := d.Decode(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -290,11 +297,10 @@ func (d *Decoder) getExtensionReader() (*bufio.Reader, error) {
|
||||||
return d.extReader, nil
|
return d.extReader, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) readChecksum(expected []byte, alreadyRead [4]byte) error {
|
func (d *Decoder) readChecksum(expected []byte) error {
|
||||||
var h plumbing.Hash
|
var h plumbing.Hash
|
||||||
copy(h[:4], alreadyRead[:])
|
|
||||||
|
|
||||||
if _, err := io.ReadFull(d.r, h[4:]); err != nil {
|
if _, err := io.ReadFull(d.r, h[:]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -476,3 +482,22 @@ func (d *endOfIndexEntryDecoder) Decode(e *EndOfIndexEntry) error {
|
||||||
_, err = io.ReadFull(d.r, e.Hash[:])
|
_, err = io.ReadFull(d.r, e.Hash[:])
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type unknownExtensionDecoder struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *unknownExtensionDecoder) Decode() error {
|
||||||
|
var buf [1024]byte
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, err := d.r.Read(buf[:])
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ package index
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||||
|
|
@ -13,7 +16,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// EncodeVersionSupported is the range of supported index versions
|
// EncodeVersionSupported is the range of supported index versions
|
||||||
EncodeVersionSupported uint32 = 3
|
EncodeVersionSupported uint32 = 4
|
||||||
|
|
||||||
// ErrInvalidTimestamp is returned by Encode if a Index with a Entry with
|
// ErrInvalidTimestamp is returned by Encode if a Index with a Entry with
|
||||||
// negative timestamp values
|
// negative timestamp values
|
||||||
|
|
@ -22,20 +25,25 @@ var (
|
||||||
|
|
||||||
// An Encoder writes an Index to an output stream.
|
// An Encoder writes an Index to an output stream.
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
hash hash.Hash
|
hash hash.Hash
|
||||||
|
lastEntry *Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder returns a new encoder that writes to w.
|
// NewEncoder returns a new encoder that writes to w.
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
h := hash.New(hash.CryptoType)
|
h := hash.New(hash.CryptoType)
|
||||||
mw := io.MultiWriter(w, h)
|
mw := io.MultiWriter(w, h)
|
||||||
return &Encoder{mw, h}
|
return &Encoder{mw, h, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode writes the Index to the stream of the encoder.
|
// Encode writes the Index to the stream of the encoder.
|
||||||
func (e *Encoder) Encode(idx *Index) error {
|
func (e *Encoder) Encode(idx *Index) error {
|
||||||
// TODO: support v4
|
return e.encode(idx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encode(idx *Index, footer bool) error {
|
||||||
|
|
||||||
// TODO: support extensions
|
// TODO: support extensions
|
||||||
if idx.Version > EncodeVersionSupported {
|
if idx.Version > EncodeVersionSupported {
|
||||||
return ErrUnsupportedVersion
|
return ErrUnsupportedVersion
|
||||||
|
|
@ -49,7 +57,10 @@ func (e *Encoder) Encode(idx *Index) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.encodeFooter()
|
if footer {
|
||||||
|
return e.encodeFooter()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) encodeHeader(idx *Index) error {
|
func (e *Encoder) encodeHeader(idx *Index) error {
|
||||||
|
|
@ -64,7 +75,7 @@ func (e *Encoder) encodeEntries(idx *Index) error {
|
||||||
sort.Sort(byName(idx.Entries))
|
sort.Sort(byName(idx.Entries))
|
||||||
|
|
||||||
for _, entry := range idx.Entries {
|
for _, entry := range idx.Entries {
|
||||||
if err := e.encodeEntry(entry); err != nil {
|
if err := e.encodeEntry(idx, entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
entryLength := entryHeaderLength
|
entryLength := entryHeaderLength
|
||||||
|
|
@ -73,7 +84,7 @@ func (e *Encoder) encodeEntries(idx *Index) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
wrote := entryLength + len(entry.Name)
|
wrote := entryLength + len(entry.Name)
|
||||||
if err := e.padEntry(wrote); err != nil {
|
if err := e.padEntry(idx, wrote); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +92,7 @@ func (e *Encoder) encodeEntries(idx *Index) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) encodeEntry(entry *Entry) error {
|
func (e *Encoder) encodeEntry(idx *Index, entry *Entry) error {
|
||||||
sec, nsec, err := e.timeToUint32(&entry.CreatedAt)
|
sec, nsec, err := e.timeToUint32(&entry.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -132,9 +143,68 @@ func (e *Encoder) encodeEntry(entry *Entry) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch idx.Version {
|
||||||
|
case 2, 3:
|
||||||
|
err = e.encodeEntryName(entry)
|
||||||
|
case 4:
|
||||||
|
err = e.encodeEntryNameV4(entry)
|
||||||
|
default:
|
||||||
|
err = ErrUnsupportedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encodeEntryName(entry *Entry) error {
|
||||||
return binary.Write(e.w, []byte(entry.Name))
|
return binary.Write(e.w, []byte(entry.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encodeEntryNameV4(entry *Entry) error {
|
||||||
|
name := entry.Name
|
||||||
|
l := 0
|
||||||
|
if e.lastEntry != nil {
|
||||||
|
dir := path.Dir(e.lastEntry.Name) + "/"
|
||||||
|
if strings.HasPrefix(entry.Name, dir) {
|
||||||
|
l = len(e.lastEntry.Name) - len(dir)
|
||||||
|
name = strings.TrimPrefix(entry.Name, dir)
|
||||||
|
} else {
|
||||||
|
l = len(e.lastEntry.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.lastEntry = entry
|
||||||
|
|
||||||
|
err := binary.WriteVariableWidthInt(e.w, int64(l))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return binary.Write(e.w, []byte(name+string('\x00')))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encodeRawExtension(signature string, data []byte) error {
|
||||||
|
if len(signature) != 4 {
|
||||||
|
return fmt.Errorf("invalid signature length")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := e.w.Write([]byte(signature))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = binary.WriteUint32(e.w, uint32(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = e.w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) {
|
func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) {
|
||||||
if t.IsZero() {
|
if t.IsZero() {
|
||||||
return 0, 0, nil
|
return 0, 0, nil
|
||||||
|
|
@ -147,7 +217,11 @@ func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) {
|
||||||
return uint32(t.Unix()), uint32(t.Nanosecond()), nil
|
return uint32(t.Unix()), uint32(t.Nanosecond()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) padEntry(wrote int) error {
|
func (e *Encoder) padEntry(idx *Index, wrote int) error {
|
||||||
|
if idx.Version == 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
padLen := 8 - wrote%8
|
padLen := 8 - wrote%8
|
||||||
|
|
||||||
_, err := e.w.Write(bytes.Repeat([]byte{'\x00'}, padLen))
|
_, err := e.w.Write(bytes.Repeat([]byte{'\x00'}, padLen))
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,17 @@ func (idx *deltaIndex) findMatch(src, tgt []byte, tgtOffset int) (srcOffset, l i
|
||||||
return 0, -1
|
return 0, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tgt) >= tgtOffset+s && len(src) >= blksz {
|
h := hashBlock(tgt, tgtOffset)
|
||||||
h := hashBlock(tgt, tgtOffset)
|
tIdx := h & idx.mask
|
||||||
tIdx := h & idx.mask
|
eIdx := idx.table[tIdx]
|
||||||
eIdx := idx.table[tIdx]
|
if eIdx == 0 {
|
||||||
if eIdx != 0 {
|
return
|
||||||
srcOffset = idx.entries[eIdx]
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l = matchLength(src, tgt, tgtOffset, srcOffset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
srcOffset = idx.entries[eIdx]
|
||||||
|
|
||||||
|
l = matchLength(src, tgt, tgtOffset, srcOffset)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,13 @@ var (
|
||||||
const (
|
const (
|
||||||
payload = 0x7f // 0111 1111
|
payload = 0x7f // 0111 1111
|
||||||
continuation = 0x80 // 1000 0000
|
continuation = 0x80 // 1000 0000
|
||||||
|
|
||||||
|
// maxPatchPreemptionSize defines what is the max size of bytes to be
|
||||||
|
// premptively made available for a patch operation.
|
||||||
|
maxPatchPreemptionSize uint = 65536
|
||||||
|
|
||||||
|
// minDeltaSize defines the smallest size for a delta.
|
||||||
|
minDeltaSize = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
type offset struct {
|
type offset struct {
|
||||||
|
|
@ -86,9 +93,13 @@ func ApplyDelta(target, base plumbing.EncodedObject, delta []byte) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchDelta returns the result of applying the modification deltas in delta to src.
|
// PatchDelta returns the result of applying the modification deltas in delta to src.
|
||||||
// An error will be returned if delta is corrupted (ErrDeltaLen) or an action command
|
// An error will be returned if delta is corrupted (ErrInvalidDelta) or an action command
|
||||||
// is not copy from source or copy from delta (ErrDeltaCmd).
|
// is not copy from source or copy from delta (ErrDeltaCmd).
|
||||||
func PatchDelta(src, delta []byte) ([]byte, error) {
|
func PatchDelta(src, delta []byte) ([]byte, error) {
|
||||||
|
if len(src) == 0 || len(delta) < minDeltaSize {
|
||||||
|
return nil, ErrInvalidDelta
|
||||||
|
}
|
||||||
|
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
if err := patchDelta(b, src, delta); err != nil {
|
if err := patchDelta(b, src, delta); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -239,7 +250,9 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||||
remainingTargetSz := targetSz
|
remainingTargetSz := targetSz
|
||||||
|
|
||||||
var cmd byte
|
var cmd byte
|
||||||
dst.Grow(int(targetSz))
|
|
||||||
|
growSz := min(targetSz, maxPatchPreemptionSize)
|
||||||
|
dst.Grow(int(growSz))
|
||||||
for {
|
for {
|
||||||
if len(delta) == 0 {
|
if len(delta) == 0 {
|
||||||
return ErrInvalidDelta
|
return ErrInvalidDelta
|
||||||
|
|
@ -403,6 +416,10 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||||
// This must be called twice on the delta data buffer, first to get the
|
// This must be called twice on the delta data buffer, first to get the
|
||||||
// expected source buffer size, and again to get the target buffer size.
|
// expected source buffer size, and again to get the target buffer size.
|
||||||
func decodeLEB128(input []byte) (uint, []byte) {
|
func decodeLEB128(input []byte) (uint, []byte) {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return 0, input
|
||||||
|
}
|
||||||
|
|
||||||
var num, sz uint
|
var num, sz uint
|
||||||
var b byte
|
var b byte
|
||||||
for {
|
for {
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,8 @@ func asciiHexToByte(b byte) (byte, error) {
|
||||||
return b - '0', nil
|
return b - '0', nil
|
||||||
case b >= 'a' && b <= 'f':
|
case b >= 'a' && b <= 'f':
|
||||||
return b - 'a' + 10, nil
|
return b - 'a' + 10, nil
|
||||||
|
case b >= 'A' && b <= 'F':
|
||||||
|
return b - 'A' + 10, nil
|
||||||
default:
|
default:
|
||||||
return 0, ErrInvalidPktLen
|
return 0, ErrInvalidPktLen
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ var (
|
||||||
// a PKCS#7 (S/MIME) signature.
|
// a PKCS#7 (S/MIME) signature.
|
||||||
x509SignatureFormat = signatureFormat{
|
x509SignatureFormat = signatureFormat{
|
||||||
[]byte("-----BEGIN CERTIFICATE-----"),
|
[]byte("-----BEGIN CERTIFICATE-----"),
|
||||||
|
[]byte("-----BEGIN SIGNED MESSAGE-----"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// sshSignatureFormat is the format of an SSH signature.
|
// sshSignatureFormat is the format of an SSH signature.
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,7 @@ func (s TreeEntrySorter) Swap(i, j int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode transforms a Tree into a plumbing.EncodedObject.
|
// Encode transforms a Tree into a plumbing.EncodedObject.
|
||||||
|
// The tree entries must be sorted by name.
|
||||||
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||||
o.SetType(plumbing.TreeObject)
|
o.SetType(plumbing.TreeObject)
|
||||||
w, err := o.Writer()
|
w, err := o.Writer()
|
||||||
|
|
|
||||||
76
vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/filter.go
generated
vendored
Normal file
76
vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/filter.go
generated
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package packp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUnsupportedObjectFilterType = errors.New("unsupported object filter type")
|
||||||
|
|
||||||
|
// Filter values enable the partial clone capability which causes
|
||||||
|
// the server to omit objects that match the filter.
|
||||||
|
//
|
||||||
|
// See [Git's documentation] for more details.
|
||||||
|
//
|
||||||
|
// [Git's documentation]: https://github.com/git/git/blob/e02ecfcc534e2021aae29077a958dd11c3897e4c/Documentation/rev-list-options.txt#L948
|
||||||
|
type Filter string
|
||||||
|
|
||||||
|
type BlobLimitPrefix string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BlobLimitPrefixNone BlobLimitPrefix = ""
|
||||||
|
BlobLimitPrefixKibi BlobLimitPrefix = "k"
|
||||||
|
BlobLimitPrefixMebi BlobLimitPrefix = "m"
|
||||||
|
BlobLimitPrefixGibi BlobLimitPrefix = "g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterBlobNone omits all blobs.
|
||||||
|
func FilterBlobNone() Filter {
|
||||||
|
return "blob:none"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterBlobLimit omits blobs of size at least n bytes (when prefix is
|
||||||
|
// BlobLimitPrefixNone), n kibibytes (when prefix is BlobLimitPrefixKibi),
|
||||||
|
// n mebibytes (when prefix is BlobLimitPrefixMebi) or n gibibytes (when
|
||||||
|
// prefix is BlobLimitPrefixGibi). n can be zero, in which case all blobs
|
||||||
|
// will be omitted.
|
||||||
|
func FilterBlobLimit(n uint64, prefix BlobLimitPrefix) Filter {
|
||||||
|
return Filter(fmt.Sprintf("blob:limit=%d%s", n, prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterTreeDepth omits all blobs and trees whose depth from the root tree
|
||||||
|
// is larger or equal to depth.
|
||||||
|
func FilterTreeDepth(depth uint64) Filter {
|
||||||
|
return Filter(fmt.Sprintf("tree:%d", depth))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterObjectType omits all objects which are not of the requested type t.
|
||||||
|
// Supported types are TagObject, CommitObject, TreeObject and BlobObject.
|
||||||
|
func FilterObjectType(t plumbing.ObjectType) (Filter, error) {
|
||||||
|
switch t {
|
||||||
|
case plumbing.TagObject:
|
||||||
|
fallthrough
|
||||||
|
case plumbing.CommitObject:
|
||||||
|
fallthrough
|
||||||
|
case plumbing.TreeObject:
|
||||||
|
fallthrough
|
||||||
|
case plumbing.BlobObject:
|
||||||
|
return Filter(fmt.Sprintf("object:type=%s", t.String())), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("%w: %s", ErrUnsupportedObjectFilterType, t.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterCombine combines multiple Filter values together.
|
||||||
|
func FilterCombine(filters ...Filter) Filter {
|
||||||
|
var escapedFilters []string
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
escapedFilters = append(escapedFilters, url.QueryEscape(string(filter)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter(fmt.Sprintf("combine:%s", strings.Join(escapedFilters, "+")))
|
||||||
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ func (d *Demuxer) nextPackData() ([]byte, error) {
|
||||||
|
|
||||||
size := len(content)
|
size := len(content)
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
return nil, nil
|
return nil, io.EOF
|
||||||
} else if size > d.max {
|
} else if size > d.max {
|
||||||
return nil, ErrMaxPackedExceeded
|
return nil, ErrMaxPackedExceeded
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,9 @@ func (r *ServerResponse) decodeACKLine(line []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
sp := bytes.Index(line, []byte(" "))
|
sp := bytes.Index(line, []byte(" "))
|
||||||
|
if sp+41 > len(line) {
|
||||||
|
return fmt.Errorf("malformed ACK %q", line)
|
||||||
|
}
|
||||||
h := plumbing.NewHash(string(line[sp+1 : sp+41]))
|
h := plumbing.NewHash(string(line[sp+1 : sp+41]))
|
||||||
r.ACKs = append(r.ACKs, h)
|
r.ACKs = append(r.ACKs, h)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type UploadRequest struct {
|
||||||
Wants []plumbing.Hash
|
Wants []plumbing.Hash
|
||||||
Shallows []plumbing.Hash
|
Shallows []plumbing.Hash
|
||||||
Depth Depth
|
Depth Depth
|
||||||
|
Filter Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Depth values stores the desired depth of the requested packfile: see
|
// Depth values stores the desired depth of the requested packfile: see
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,17 @@ func (e *ulReqEncoder) encodeDepth() stateFn {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return e.encodeFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ulReqEncoder) encodeFilter() stateFn {
|
||||||
|
if filter := e.data.Filter; filter != "" {
|
||||||
|
if err := e.pe.Encodef("filter %s\n", filter); err != nil {
|
||||||
|
e.err = fmt.Errorf("encoding filter %s: %s", filter, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return e.encodeFlush
|
return e.encodeFlush
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ func (r ReferenceName) Validate() error {
|
||||||
|
|
||||||
isBranch := r.IsBranch()
|
isBranch := r.IsBranch()
|
||||||
isTag := r.IsTag()
|
isTag := r.IsTag()
|
||||||
for _, part := range parts {
|
for i, part := range parts {
|
||||||
// rule 6
|
// rule 6
|
||||||
if len(part) == 0 {
|
if len(part) == 0 {
|
||||||
return ErrInvalidReferenceName
|
return ErrInvalidReferenceName
|
||||||
|
|
@ -205,7 +205,7 @@ func (r ReferenceName) Validate() error {
|
||||||
return ErrInvalidReferenceName
|
return ErrInvalidReferenceName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBranch || isTag) && strings.HasPrefix(part, "-") { // branches & tags can't start with -
|
if (isBranch || isTag) && strings.HasPrefix(part, "-") && (i == 2) { // branches & tags can't start with -
|
||||||
return ErrInvalidReferenceName
|
return ErrInvalidReferenceName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -295,7 +296,11 @@ func parseFile(endpoint string) (*Endpoint, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
path := endpoint
|
path, err := filepath.Abs(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
return &Endpoint{
|
return &Endpoint{
|
||||||
Protocol: "file",
|
Protocol: "file",
|
||||||
Path: path,
|
Path: path,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
|
|
@ -95,7 +96,23 @@ func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.Auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &command{cmd: execabs.Command(cmd, ep.Path)}, nil
|
return &command{cmd: execabs.Command(cmd, adjustPathForWindows(ep.Path))}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDriveLetter(c byte) bool {
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, the path that results from a file: URL has a leading slash. This
|
||||||
|
// has to be removed if there's a drive letter
|
||||||
|
func adjustPathForWindows(p string) string {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == ':' {
|
||||||
|
return p[1:]
|
||||||
|
}
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
type command struct {
|
type command struct {
|
||||||
|
|
|
||||||
|
|
@ -430,11 +430,11 @@ func NewErr(r *http.Response) error {
|
||||||
|
|
||||||
switch r.StatusCode {
|
switch r.StatusCode {
|
||||||
case http.StatusUnauthorized:
|
case http.StatusUnauthorized:
|
||||||
return transport.ErrAuthenticationRequired
|
return fmt.Errorf("%w: %s", transport.ErrAuthenticationRequired, reason)
|
||||||
case http.StatusForbidden:
|
case http.StatusForbidden:
|
||||||
return transport.ErrAuthorizationFailed
|
return fmt.Errorf("%w: %s", transport.ErrAuthorizationFailed, reason)
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
return transport.ErrRepositoryNotFound
|
return fmt.Errorf("%w: %s", transport.ErrRepositoryNotFound, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
return plumbing.NewUnexpectedError(&Err{r, reason})
|
return plumbing.NewUnexpectedError(&Err{r, reason})
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,16 @@ func (l *fsLoader) Load(ep *transport.Endpoint) (storer.Storer, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.Stat("config"); err != nil {
|
var bare bool
|
||||||
return nil, transport.ErrRepositoryNotFound
|
if _, err := fs.Stat("config"); err == nil {
|
||||||
|
bare = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bare {
|
||||||
|
// do not use git.GitDirName due to import cycle
|
||||||
|
if _, err := fs.Stat(".git"); err != nil {
|
||||||
|
return nil, transport.ErrRepositoryNotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesystem.NewStorage(fs, cache.NewObjectLRUDefault()), nil
|
return filesystem.NewStorage(fs, cache.NewObjectLRUDefault()), nil
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-billy/v5/osfs"
|
"github.com/go-git/go-billy/v5/osfs"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/config"
|
"github.com/go-git/go-git/v5/config"
|
||||||
"github.com/go-git/go-git/v5/internal/url"
|
"github.com/go-git/go-git/v5/internal/url"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
|
@ -82,7 +83,7 @@ func (r *Remote) String() string {
|
||||||
var fetch, push string
|
var fetch, push string
|
||||||
if len(r.c.URLs) > 0 {
|
if len(r.c.URLs) > 0 {
|
||||||
fetch = r.c.URLs[0]
|
fetch = r.c.URLs[0]
|
||||||
push = r.c.URLs[0]
|
push = r.c.URLs[len(r.c.URLs)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s\t%s (fetch)\n%[1]s\t%[3]s (push)", r.c.Name, fetch, push)
|
return fmt.Sprintf("%s\t%s (fetch)\n%[1]s\t%[3]s (push)", r.c.Name, fetch, push)
|
||||||
|
|
@ -109,8 +110,8 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) {
|
||||||
return fmt.Errorf("remote names don't match: %s != %s", o.RemoteName, r.c.Name)
|
return fmt.Errorf("remote names don't match: %s != %s", o.RemoteName, r.c.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.RemoteURL == "" {
|
if o.RemoteURL == "" && len(r.c.URLs) > 0 {
|
||||||
o.RemoteURL = r.c.URLs[0]
|
o.RemoteURL = r.c.URLs[len(r.c.URLs)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions)
|
s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions)
|
||||||
|
|
@ -491,7 +492,18 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updated && !updatedPrune {
|
if !updated && !updatedPrune {
|
||||||
return remoteRefs, NoErrAlreadyUpToDate
|
// No references updated, but may have fetched new objects, check if we now have any of our wants
|
||||||
|
for _, hash := range req.Wants {
|
||||||
|
exists, _ := objectExists(r.s, hash)
|
||||||
|
if exists {
|
||||||
|
updated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated {
|
||||||
|
return remoteRefs, NoErrAlreadyUpToDate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return remoteRefs, nil
|
return remoteRefs, nil
|
||||||
|
|
@ -878,17 +890,12 @@ func getHavesFromRef(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to load the commit if we know the remote already
|
|
||||||
// has this hash.
|
|
||||||
if remoteRefs[h] {
|
|
||||||
haves[h] = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := object.GetCommit(s, h)
|
commit, err := object.GetCommit(s, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Ignore the error if this isn't a commit.
|
if !errors.Is(err, plumbing.ErrObjectNotFound) {
|
||||||
haves[ref.Hash()] = true
|
// Ignore the error if this isn't a commit.
|
||||||
|
haves[ref.Hash()] = true
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -956,7 +956,7 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.RecurseSubmodules != NoRecurseSubmodules {
|
if o.RecurseSubmodules != NoRecurseSubmodules {
|
||||||
if err := w.updateSubmodules(&SubmoduleUpdateOptions{
|
if err := w.updateSubmodules(ctx, &SubmoduleUpdateOptions{
|
||||||
RecurseSubmodules: o.RecurseSubmodules,
|
RecurseSubmodules: o.RecurseSubmodules,
|
||||||
Depth: func() int {
|
Depth: func() int {
|
||||||
if o.ShallowSubmodules {
|
if o.ShallowSubmodules {
|
||||||
|
|
@ -1037,7 +1037,7 @@ func (r *Repository) setIsBare(isBare bool) error {
|
||||||
return r.Storer.SetConfig(cfg)
|
return r.Storer.SetConfig(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) updateRemoteConfigIfNeeded(o *CloneOptions, c *config.RemoteConfig, head *plumbing.Reference) error {
|
func (r *Repository) updateRemoteConfigIfNeeded(o *CloneOptions, c *config.RemoteConfig, _ *plumbing.Reference) error {
|
||||||
if !o.SingleBranch {
|
if !o.SingleBranch {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
mindex "github.com/go-git/go-git/v5/utils/merkletrie/index"
|
||||||
|
"github.com/go-git/go-git/v5/utils/merkletrie/noder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status represents the current status of a Worktree.
|
// Status represents the current status of a Worktree.
|
||||||
|
|
@ -77,3 +80,69 @@ const (
|
||||||
Copied StatusCode = 'C'
|
Copied StatusCode = 'C'
|
||||||
UpdatedButUnmerged StatusCode = 'U'
|
UpdatedButUnmerged StatusCode = 'U'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StatusStrategy defines the different types of strategies when processing
|
||||||
|
// the worktree status.
|
||||||
|
type StatusStrategy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TODO: (V6) Review the default status strategy.
|
||||||
|
// TODO: (V6) Review the type used to represent Status, to enable lazy
|
||||||
|
// processing of statuses going direct to the backing filesystem.
|
||||||
|
defaultStatusStrategy = Empty
|
||||||
|
|
||||||
|
// Empty starts its status map from empty. Missing entries for a given
|
||||||
|
// path means that the file is untracked. This causes a known issue (#119)
|
||||||
|
// whereby unmodified files can be incorrectly reported as untracked.
|
||||||
|
//
|
||||||
|
// This can be used when returning the changed state within a modified Worktree.
|
||||||
|
// For example, to check whether the current worktree is clean.
|
||||||
|
Empty StatusStrategy = 0
|
||||||
|
// Preload goes through all existing nodes from the index and add them to the
|
||||||
|
// status map as unmodified. This is currently the most reliable strategy
|
||||||
|
// although it comes at a performance cost in large repositories.
|
||||||
|
//
|
||||||
|
// This method is recommended when fetching the status of unmodified files.
|
||||||
|
// For example, to confirm the status of a specific file that is either
|
||||||
|
// untracked or unmodified.
|
||||||
|
Preload StatusStrategy = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s StatusStrategy) new(w *Worktree) (Status, error) {
|
||||||
|
switch s {
|
||||||
|
case Preload:
|
||||||
|
return preloadStatus(w)
|
||||||
|
case Empty:
|
||||||
|
return make(Status), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %+v", ErrUnsupportedStatusStrategy, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preloadStatus(w *Worktree) (Status, error) {
|
||||||
|
idx, err := w.r.Storer.Index()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idxRoot := mindex.NewRootNode(idx)
|
||||||
|
nodes := []noder.Noder{idxRoot}
|
||||||
|
|
||||||
|
status := make(Status)
|
||||||
|
for len(nodes) > 0 {
|
||||||
|
var node noder.Noder
|
||||||
|
node, nodes = nodes[0], nodes[1:]
|
||||||
|
if node.IsDir() {
|
||||||
|
children, err := node.Children()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nodes = append(nodes, children...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fs := status.File(node.Name())
|
||||||
|
fs.Worktree = Unmodified
|
||||||
|
fs.Staging = Unmodified
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue