diff --git a/.github/actions/build-gosop/action.yml b/.github/actions/build-gosop/action.yml new file mode 100644 index 00000000..5f4fa763 --- /dev/null +++ b/.github/actions/build-gosop/action.yml @@ -0,0 +1,55 @@ +name: 'build-gosop' +description: 'Build gosop from the current branch' + +inputs: + + go-crypto-ref: + description: 'go-crypto branch tag or commit to build from' + required: true + default: '' + + binary-location: + description: 'Path for the gosop binary' + required: true + default: './gosop-${{ github.sha }}' + +runs: + using: "composite" + steps: + - name: Checkout go-crypto + uses: actions/checkout@v3 + with: + ref: ${{ inputs.go-crypto-ref }} + path: go-crypto + # Build gosop + - name: Set up latest golang + uses: actions/setup-go@v3 + with: + go-version: ^1.18 + - name: Check out gosop + uses: actions/checkout@v3 + with: + repository: ProtonMail/gosop + path: gosop + - name: Cache go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Build gosop + run: ./.github/test-suite/build_gosop.sh + shell: bash + # Test the binary + - name: Print gosop version + run: ./gosop/gosop version --extended + shell: bash + # Move and rename binary + - name: Move binary + run: mv gosop/gosop ${{ inputs.binary-location }} + shell: bash + + \ No newline at end of file diff --git a/.github/test-suite/build_gosop.sh b/.github/test-suite/build_gosop.sh new file mode 100755 index 00000000..e528d36b --- /dev/null +++ b/.github/test-suite/build_gosop.sh @@ -0,0 +1,5 @@ +cd gosop +echo "replace github.com/ProtonMail/go-crypto => ../go-crypto" >> go.mod +go get github.com/ProtonMail/go-crypto +go get github.com/ProtonMail/gopenpgp/v2/crypto@latest +go build . diff --git a/.github/test-suite/config.json.template b/.github/test-suite/config.json.template new file mode 100644 index 00000000..ac3701f3 --- /dev/null +++ b/.github/test-suite/config.json.template @@ -0,0 +1,28 @@ +{ + "drivers": [ + { + "id": "gosop-branch", + "path": "__GOSOP_BRANCH__" + }, + { + "id": "gosop-main", + "path": "__GOSOP_MAIN__" + }, + { + "path": "__SQOP__" + }, + { + "path": "__GPGME_SOP__" + }, + { + "path": "__SOP_OPENPGPJS__" + }, + { + "path": "__RNP_SOP__" + } + ], + "rlimits": { + "DATA": 1073741824 + } +} + \ No newline at end of file diff --git a/.github/test-suite/prepare_config.sh b/.github/test-suite/prepare_config.sh new file mode 100755 index 00000000..ae49d01f --- /dev/null +++ b/.github/test-suite/prepare_config.sh @@ -0,0 +1,12 @@ +CONFIG_TEMPLATE=$1 +CONFIG_OUTPUT=$2 +GOSOP_BRANCH=$3 +GOSOP_MAIN=$4 +cat $CONFIG_TEMPLATE \ + | sed "s@__GOSOP_BRANCH__@${GOSOP_BRANCH}@g" \ + | sed "s@__GOSOP_MAIN__@${GOSOP_MAIN}@g" \ + | sed "s@__SQOP__@${SQOP}@g" \ + | sed "s@__GPGME_SOP__@${GPGME_SOP}@g" \ + | sed "s@__SOP_OPENPGPJS__@${SOP_OPENPGPJS}@g" \ + | sed "s@__RNP_SOP__@${RNP_SOP}@g" \ + > $CONFIG_OUTPUT \ No newline at end of file diff --git a/.github/workflows/interop-test-suite.yml b/.github/workflows/interop-test-suite.yml new file mode 100644 index 00000000..9d6fb0d2 --- /dev/null +++ b/.github/workflows/interop-test-suite.yml @@ -0,0 +1,125 @@ +name: SOP interoperability test suite + +on: + pull_request: + branches: [ main ] + +jobs: + + build-gosop: + name: Build gosop from branch + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build gosop from branch + uses: ./.github/actions/build-gosop + with: + binary-location: ./gosop-${{ github.sha }} + # Upload as artifact + - name: Upload gosop artifact + uses: actions/upload-artifact@v3 + with: + name: gosop-${{ github.sha }} + path: ./gosop-${{ github.sha }} + + build-gosop-main: + name: Build gosop from main + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build gosop from branch + uses: ./.github/actions/build-gosop + with: + go-crypto-ref: main + binary-location: ./gosop-main + # Upload as artifact + - name: Upload gosop-main artifact + uses: actions/upload-artifact@v3 + with: + name: gosop-main + path: ./gosop-main + + + test-suite: + name: Run interoperability test suite + runs-on: ubuntu-latest + container: + image: ghcr.io/protonmail/openpgp-interop-test-docker:v1.1.1 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.github_token }} + needs: + - build-gosop + - build-gosop-main + steps: + - name: Checkout + uses: actions/checkout@v3 + # Fetch gosop from main + - name: Download gosop-main + uses: actions/download-artifact@v3 + with: + name: gosop-main + # Test gosop-main + - name: Make gosop-main executable + run: chmod +x gosop-main + - name: Print gosop-main version + run: ./gosop-main version --extended + # Fetch gosop from branch + - name: Download gosop-branch + uses: actions/download-artifact@v3 + with: + name: gosop-${{ github.sha }} + - name: Rename gosop-branch + run: mv gosop-${{ github.sha }} gosop-branch + # Test gosop-branch + - name: Make gosop-branch executable + run: chmod +x gosop-branch + - name: Print gosop-branch version + run: ./gosop-branch version --extended + # Run test suite + - name: Prepare test configuration + run: ./.github/test-suite/prepare_config.sh $CONFIG_TEMPLATE $CONFIG_OUTPUT $GITHUB_WORKSPACE/gosop-branch $GITHUB_WORKSPACE/gosop-main + env: + CONFIG_TEMPLATE: .github/test-suite/config.json.template + CONFIG_OUTPUT: .github/test-suite/config.json + - name: Display configuration + run: cat .github/test-suite/config.json + - name: Run interoperability test suite + run: cd $TEST_SUITE_DIR && $TEST_SUITE --config $GITHUB_WORKSPACE/$CONFIG --json-out $GITHUB_WORKSPACE/$RESULTS_JSON --html-out $GITHUB_WORKSPACE/$RESULTS_HTML + env: + CONFIG: .github/test-suite/config.json + RESULTS_JSON: .github/test-suite/test-suite-results.json + RESULTS_HTML: .github/test-suite/test-suite-results.html + # Upload results + - name: Upload test results json artifact + uses: actions/upload-artifact@v3 + with: + name: test-suite-results.json + path: .github/test-suite/test-suite-results.json + - name: Upload test results html artifact + uses: actions/upload-artifact@v3 + with: + name: test-suite-results.html + path: .github/test-suite/test-suite-results.html + + compare-with-main: + name: Compare with main + runs-on: ubuntu-latest + needs: test-suite + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Download test results json artifact + id: download-test-results + uses: actions/download-artifact@v3 + with: + name: test-suite-results.json + - name: Compare with baseline + uses: ProtonMail/openpgp-interop-test-analyzer@v1 + with: + results: ${{ steps.download-test-results.outputs.download-path }}/test-suite-results.json + output: baseline-comparison.json + baseline: gosop-main + target: gosop-branch diff --git a/bitcurves/bitcurve.go b/bitcurves/bitcurve.go index 3ed3f435..c85e6bef 100644 --- a/bitcurves/bitcurve.go +++ b/bitcurves/bitcurve.go @@ -191,7 +191,7 @@ func (bitCurve *BitCurve) doubleJacobian(x, y, z *big.Int) (*big.Int, *big.Int, return x3, y3, z3 } -//TODO: double check if it is okay +// TODO: double check if it is okay // ScalarMult returns k*(Bx,By) where k is a number in big-endian form. func (bitCurve *BitCurve) ScalarMult(Bx, By *big.Int, k []byte) (*big.Int, *big.Int) { // We have a slight problem in that the identity of the group (the @@ -239,7 +239,7 @@ func (bitCurve *BitCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) { var mask = []byte{0xff, 0x1, 0x3, 0x7, 0xf, 0x1f, 0x3f, 0x7f} -//TODO: double check if it is okay +// TODO: double check if it is okay // GenerateKey returns a public/private key pair. The private key is generated // using the given reader, which must return random data. func (bitCurve *BitCurve) GenerateKey(rand io.Reader) (priv []byte, x, y *big.Int, err error) { diff --git a/brainpool/brainpool_test.go b/brainpool/brainpool_test.go index c6375fde..41b369d5 100644 --- a/brainpool/brainpool_test.go +++ b/brainpool/brainpool_test.go @@ -46,4 +46,4 @@ func TestP512t1(t *testing.T) { func TestP512r1(t *testing.T) { testCurve(t, P512r1()) -} \ No newline at end of file +} diff --git a/brainpool/rcurve.go b/brainpool/rcurve.go index 2d535508..7e291d6a 100644 --- a/brainpool/rcurve.go +++ b/brainpool/rcurve.go @@ -80,4 +80,4 @@ func (curve *rcurve) ScalarMult(x1, y1 *big.Int, scalar []byte) (x, y *big.Int) func (curve *rcurve) ScalarBaseMult(scalar []byte) (x, y *big.Int) { return curve.fromTwisted(curve.twisted.ScalarBaseMult(scalar)) -} \ No newline at end of file +} diff --git a/eax/eax.go b/eax/eax.go index 6b6bc7ae..3ae91d59 100644 --- a/eax/eax.go +++ b/eax/eax.go @@ -67,7 +67,7 @@ func (e *eax) Seal(dst, nonce, plaintext, adata []byte) []byte { if len(nonce) > e.nonceSize { panic("crypto/eax: Nonce too long for this instance") } - ret, out := byteutil.SliceForAppend(dst, len(plaintext) + e.tagSize) + ret, out := byteutil.SliceForAppend(dst, len(plaintext)+e.tagSize) omacNonce := e.omacT(0, nonce) omacAdata := e.omacT(1, adata) @@ -85,7 +85,7 @@ func (e *eax) Seal(dst, nonce, plaintext, adata []byte) []byte { return ret } -func (e* eax) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) { +func (e *eax) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) { if len(nonce) > e.nonceSize { panic("crypto/eax: Nonce too long for this instance") } diff --git a/go.mod b/go.mod index d8766e1b..3e49a137 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/ProtonMail/go-crypto go 1.13 require ( - github.com/cloudflare/circl v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + github.com/cloudflare/circl v1.3.3 + golang.org/x/crypto v0.7.0 ) diff --git a/go.sum b/go.sum index 7fe4589a..90f8b274 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,46 @@ -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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-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.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +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/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.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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= diff --git a/internal/byteutil/byteutil.go b/internal/byteutil/byteutil.go index a6bdf512..affb74a7 100644 --- a/internal/byteutil/byteutil.go +++ b/internal/byteutil/byteutil.go @@ -41,7 +41,7 @@ func ShiftNBytesLeft(dst, x []byte, n int) { bits := uint(n % 8) l := len(dst) for i := 0; i < l-1; i++ { - dst[i] = (dst[i] << bits) | (dst[i+1] >> uint(8 - bits)) + dst[i] = (dst[i] << bits) | (dst[i+1] >> uint(8-bits)) } dst[l-1] = dst[l-1] << bits @@ -56,7 +56,6 @@ func XorBytesMut(X, Y []byte) { } } - // XorBytes assumes equal input length, puts X XOR Y into Z func XorBytes(Z, X, Y []byte) { for i := 0; i < len(X); i++ { @@ -67,10 +66,10 @@ func XorBytes(Z, X, Y []byte) { // RightXor XORs smaller input (assumed Y) at the right of the larger input (assumed X) func RightXor(X, Y []byte) []byte { offset := len(X) - len(Y) - xored := make([]byte, len(X)); + xored := make([]byte, len(X)) copy(xored, X) for i := 0; i < len(Y); i++ { - xored[offset + i] ^= Y[i] + xored[offset+i] ^= Y[i] } return xored } @@ -89,4 +88,3 @@ func SliceForAppend(in []byte, n int) (head, tail []byte) { tail = head[len(in):] return } - diff --git a/ocb/ocb.go b/ocb/ocb.go index 7f78cfa7..1a6f7350 100644 --- a/ocb/ocb.go +++ b/ocb/ocb.go @@ -93,13 +93,13 @@ func NewOCBWithNonceAndTagSize( return nil, ocbError("Custom tag length exceeds blocksize") } return &ocb{ - block: block, - tagSize: tagSize, - nonceSize: nonceSize, - mask: initializeMaskTable(block), + block: block, + tagSize: tagSize, + nonceSize: nonceSize, + mask: initializeMaskTable(block), reusableKtop: reusableKtop{ noncePrefix: nil, - Ktop: nil, + Ktop: nil, }, }, nil } diff --git a/ocb/rfc7253_test_vectors_suite_b.go b/ocb/rfc7253_test_vectors_suite_b.go index 5dc158f0..14a3c336 100644 --- a/ocb/rfc7253_test_vectors_suite_b.go +++ b/ocb/rfc7253_test_vectors_suite_b.go @@ -4,21 +4,22 @@ package ocb var rfc7253TestVectorTaglen96 = struct { key, nonce, header, plaintext, ciphertext string }{"0F0E0D0C0B0A09080706050403020100", - "BBAA9988776655443322110D", - "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627", - "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627", - "1792A4E31E0755FB03E31B22116E6C2DDF9EFD6E33D536F1A0124B0A55BAE884ED93481529C76B6AD0C515F4D1CDD4FDAC4F02AA"} + "BBAA9988776655443322110D", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627", + "1792A4E31E0755FB03E31B22116E6C2DDF9EFD6E33D536F1A0124B0A55BAE884ED93481529C76B6AD0C515F4D1CDD4FDAC4F02AA"} var rfc7253AlgorithmTest = []struct { KEYLEN, TAGLEN int - OUTPUT string }{ - {128, 128, "67E944D23256C5E0B6C61FA22FDF1EA2"}, - {192, 128, "F673F2C3E7174AAE7BAE986CA9F29E17"}, - {256, 128, "D90EB8E9C977C88B79DD793D7FFA161C"}, - {128, 96, "77A3D8E73589158D25D01209"}, - {192, 96, "05D56EAD2752C86BE6932C5E"}, - {256, 96, "5458359AC23B0CBA9E6330DD"}, - {128, 64, "192C9B7BD90BA06A"}, - {192, 64, "0066BC6E0EF34E24"}, - {256, 64, "7D4EA5D445501CBE"}, - } + OUTPUT string +}{ + {128, 128, "67E944D23256C5E0B6C61FA22FDF1EA2"}, + {192, 128, "F673F2C3E7174AAE7BAE986CA9F29E17"}, + {256, 128, "D90EB8E9C977C88B79DD793D7FFA161C"}, + {128, 96, "77A3D8E73589158D25D01209"}, + {192, 96, "05D56EAD2752C86BE6932C5E"}, + {256, 96, "5458359AC23B0CBA9E6330DD"}, + {128, 64, "192C9B7BD90BA06A"}, + {192, 64, "0066BC6E0EF34E24"}, + {256, 64, "7D4EA5D445501CBE"}, +} diff --git a/openpgp/armor/armor.go b/openpgp/armor/armor.go index 3b357e58..d7af9141 100644 --- a/openpgp/armor/armor.go +++ b/openpgp/armor/armor.go @@ -10,19 +10,22 @@ import ( "bufio" "bytes" "encoding/base64" - "github.com/ProtonMail/go-crypto/openpgp/errors" "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) // A Block represents an OpenPGP armored structure. // // The encoded form is: -// -----BEGIN Type----- -// Headers // -// base64-encoded Bytes -// '=' base64 encoded checksum -// -----END Type----- +// -----BEGIN Type----- +// Headers +// +// base64-encoded Bytes +// '=' base64 encoded checksum +// -----END Type----- +// // where Headers is a possibly empty sequence of Key: Value lines. // // Since the armored data can be very large, this package presents a streaming @@ -206,12 +209,16 @@ TryNextBlock: break } - i := bytes.Index(line, []byte(": ")) + i := bytes.Index(line, []byte(":")) if i == -1 { goto TryNextBlock } lastKey = string(line[:i]) - p.Header[lastKey] = string(line[i+2:]) + var value string + if len(line) > i+2 { + value = string(line[i+2:]) + } + p.Header[lastKey] = value } p.lReader.in = r diff --git a/openpgp/armor/armor_test.go b/openpgp/armor/armor_test.go index 9334e94e..59561249 100644 --- a/openpgp/armor/armor_test.go +++ b/openpgp/armor/armor_test.go @@ -50,7 +50,26 @@ func TestDecodeEncode(t *testing.T) { w.Close() if !bytes.Equal(buf.Bytes(), []byte(armorExample1)) { - t.Errorf("got: %s\nwant: %s", string(buf.Bytes()), armorExample1) + t.Errorf("got: %s\nwant: %s", buf.String(), armorExample1) + } +} + +func TestDecodeEmptyVersion(t *testing.T) { + buf := bytes.NewBuffer([]byte(armorExampleEmptyVersion)) + result, err := Decode(buf) + if err != nil { + t.Error(err) + } + expectedType := "PGP SIGNATURE" + if result.Type != expectedType { + t.Errorf("result.Type: got:%s want:%s", result.Type, expectedType) + } + if len(result.Header) != 1 { + t.Errorf("len(result.Header): got:%d want:1", len(result.Header)) + } + v, ok := result.Header["Version"] + if !ok || v != "" { + t.Errorf("result.Header: got:%#v", result.Header) } } @@ -93,3 +112,20 @@ okWuf3+xA9ksp1npSY/mDvgHijmjvtpRDe6iUeqfCn8N9u9CBg8geANgaG8+QA4= -----END PGP SIGNATURE-----` const longValueExpected = "0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz" + +const armorExampleEmptyVersion = `-----BEGIN PGP SIGNATURE----- +Version: + +wsE7BAABCgBvBYJkbfmWCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmeMXzsJEgIm228SdxV22XgYny4adwqEgyIT9UL3F92C +OhYhBNGmbhojsYLJmA94jPv8yCoBXnMwAAAj1AwAiSkJPxsEcyaoYWbxc657xPW1 +MlrbNhDBIWpKVrqQgyz7NdDZvvY0Ty+/h62HK5GQ5obAzVmQVwtUVG950TxCksg1 +F18mqticpxg1veZQdw7DBYTk0RJTpdVBRYJ5UOtHaSJUAnqGh7OQE6Lu74vfFhNv +dDjpjgEc6TnJrEBOOR7+RVp7+0i4HhM3+JdfSOMMOEb6ixWEYLtfC2Zd/p0f7vP8 +tHiqllDXDbfBCXlFl5h2LAh39o/LE0vZvwf+C9i9PgRARawWIh+xeAJsVne8FZ12 +FD+hWZJdNUCv4iE1H7QDVv8nuPAz3WB/DQWNSfeGTZnN+ouB1cjPFscBuunO5Dss +k3hXy+XB5mZW6iisjUnUBknJEa43AMX+zGSaGHljEgfTGLbgEK+deOhPqKEkhUKr +/VlIVBXgfjQuoizme9S9juxXHdDHa+Y5Wb9rTUc1y9YPArRem51VI0OzbJ2cRnLH +J0YF6lYvjcTVBtmQlYeOfZsz4EABEeBYe/rbDmJC +=b+IB +-----END PGP SIGNATURE-----` diff --git a/openpgp/armor/encode.go b/openpgp/armor/encode.go index 6f07582c..5b6e16c1 100644 --- a/openpgp/armor/encode.go +++ b/openpgp/armor/encode.go @@ -96,7 +96,8 @@ func (l *lineBreaker) Close() (err error) { // trailer. // // It's built into a stack of io.Writers: -// encoding -> base64 encoder -> lineBreaker -> out +// +// encoding -> base64 encoder -> lineBreaker -> out type encoding struct { out io.Writer breaker *lineBreaker diff --git a/openpgp/clearsign/clearsign.go b/openpgp/clearsign/clearsign.go index 9f695623..8f70b373 100644 --- a/openpgp/clearsign/clearsign.go +++ b/openpgp/clearsign/clearsign.go @@ -307,6 +307,10 @@ func (d *dashEscaper) Close() (err error) { sig.Hash = d.hashType sig.CreationTime = t sig.IssuerKeyId = &k.KeyId + sig.IssuerFingerprint = k.Fingerprint + sig.Notations = d.config.Notations() + sigLifetimeSecs := d.config.SigLifetime() + sig.SigLifetimeSecs = &sigLifetimeSecs if err = sig.Sign(d.hashers[i], k, d.config); err != nil { return @@ -421,12 +425,6 @@ func (b *Block) VerifySignature(keyring openpgp.KeyRing, config *packet.Config) // if the name isn't known. See RFC 4880, section 9.4. func nameOfHash(h crypto.Hash) string { switch h { - case crypto.MD5: - return "MD5" - case crypto.SHA1: - return "SHA1" - case crypto.RIPEMD160: - return "RIPEMD160" case crypto.SHA224: return "SHA224" case crypto.SHA256: @@ -447,12 +445,8 @@ func nameOfHash(h crypto.Hash) string { // if the name isn't known. See RFC 4880, section 9.4. func nameToHash(h string) crypto.Hash { switch h { - case "MD5": - return crypto.MD5 case "SHA1": return crypto.SHA1 - case "RIPEMD160": - return crypto.RIPEMD160 case "SHA224": return crypto.SHA224 case "SHA256": diff --git a/openpgp/clearsign/clearsign_test.go b/openpgp/clearsign/clearsign_test.go index d77c7652..ffd5660f 100644 --- a/openpgp/clearsign/clearsign_test.go +++ b/openpgp/clearsign/clearsign_test.go @@ -6,9 +6,9 @@ package clearsign import ( "bytes" + "fmt" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" - "fmt" "io" "testing" ) diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index b09e2a73..c895bad6 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -34,7 +34,7 @@ type PrivateKey struct { func NewPublicKey(curve ecc.ECDHCurve, kdfHash algorithm.Hash, kdfCipher algorithm.Cipher) *PublicKey { return &PublicKey{ - curve: curve, + curve: curve, KDF: KDF{ Hash: kdfHash, Cipher: kdfCipher, @@ -167,7 +167,7 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead if _, err := param.Write(fingerprint[:20]); err != nil { return nil, err } - if param.Len() - len(curveOID) != 45 { + if param.Len()-len(curveOID) != 45 { return nil, errors.New("ecdh: malformed KDF Param") } @@ -181,15 +181,19 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead j := zbLen - 1 if stripLeading { // Work around old go crypto bug where the leading zeros are missing. - for ; i < zbLen && zb[i] == 0; i++ {} + for i < zbLen && zb[i] == 0 { + i++ + } } if stripTrailing { // Work around old OpenPGP.js bug where insignificant trailing zeros in // this little-endian number are missing. // (See https://github.com/openpgpjs/openpgpjs/pull/853.) - for ; j >= 0 && zb[j] == 0; j-- {} + for j >= 0 && zb[j] == 0 { + j-- + } } - if _, err := h.Write(zb[i:j+1]); err != nil { + if _, err := h.Write(zb[i : j+1]); err != nil { return nil, err } if _, err := h.Write(param.Bytes()); err != nil { diff --git a/openpgp/ecdh/ecdh_test.go b/openpgp/ecdh/ecdh_test.go index 18dca8ac..1f70b7dd 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -73,7 +73,6 @@ func testEncryptDecrypt(t *testing.T, priv *PrivateKey, oid, fingerprint []byte) } } - func testValidation(t *testing.T, priv *PrivateKey) { if err := Validate(priv); err != nil { t.Fatalf("valid key marked as invalid: %s", err) diff --git a/openpgp/ecdsa/ecdsa.go b/openpgp/ecdsa/ecdsa.go index 6682a21a..f94ae1b2 100644 --- a/openpgp/ecdsa/ecdsa.go +++ b/openpgp/ecdsa/ecdsa.go @@ -10,7 +10,7 @@ import ( ) type PublicKey struct { - X, Y *big.Int + X, Y *big.Int curve ecc.ECDSACurve } diff --git a/openpgp/ecdsa/ecdsa_test.go b/openpgp/ecdsa/ecdsa_test.go index 8280305c..0dc904d2 100644 --- a/openpgp/ecdsa/ecdsa_test.go +++ b/openpgp/ecdsa/ecdsa_test.go @@ -25,7 +25,6 @@ func TestCurves(t *testing.T) { t.Fatal(err) } - priv := testGenerate(t, ECDSACurve) testSignVerify(t, priv) testValidation(t, priv) @@ -38,7 +37,7 @@ func TestCurves(t *testing.T) { } func testGenerate(t *testing.T, curve ecc.ECDSACurve) *PrivateKey { - priv, err := GenerateKey( rand.Reader, curve) + priv, err := GenerateKey(rand.Reader, curve) if err != nil { t.Fatal(err) } @@ -90,7 +89,7 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { t.Fatalf("unable to unmarshal integer: %s", err) } - if priv.X.Cmp(parsed.X) != 0 || priv.Y.Cmp(parsed.Y) != 0 || priv.D.Cmp(parsed.D) != 0{ + if priv.X.Cmp(parsed.X) != 0 || priv.Y.Cmp(parsed.Y) != 0 || priv.D.Cmp(parsed.D) != 0 { t.Fatal("failed to marshal/unmarshal correctly") } } diff --git a/openpgp/eddsa/eddsa.go b/openpgp/eddsa/eddsa.go index 12866c12..99ecfc7f 100644 --- a/openpgp/eddsa/eddsa.go +++ b/openpgp/eddsa/eddsa.go @@ -9,7 +9,7 @@ import ( ) type PublicKey struct { - X []byte + X []byte curve ecc.EdDSACurve } diff --git a/openpgp/elgamal/elgamal.go b/openpgp/elgamal/elgamal.go index 6a07d8ff..bad27743 100644 --- a/openpgp/elgamal/elgamal.go +++ b/openpgp/elgamal/elgamal.go @@ -71,8 +71,8 @@ func Encrypt(random io.Reader, pub *PublicKey, msg []byte) (c1, c2 *big.Int, err // returns the plaintext of the message. An error can result only if the // ciphertext is invalid. Users should keep in mind that this is a padding // oracle and thus, if exposed to an adaptive chosen ciphertext attack, can -// be used to break the cryptosystem. See ``Chosen Ciphertext Attacks -// Against Protocols Based on the RSA Encryption Standard PKCS #1'', Daniel +// be used to break the cryptosystem. See “Chosen Ciphertext Attacks +// Against Protocols Based on the RSA Encryption Standard PKCS #1”, Daniel // Bleichenbacher, Advances in Cryptology (Crypto '98), func Decrypt(priv *PrivateKey, c1, c2 *big.Int) (msg []byte, err error) { s := new(big.Int).Exp(c1, priv.X, priv.P) diff --git a/openpgp/hash.go b/openpgp/hash.go new file mode 100644 index 00000000..526bd777 --- /dev/null +++ b/openpgp/hash.go @@ -0,0 +1,24 @@ +package openpgp + +import ( + "crypto" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +// HashIdToHash returns a crypto.Hash which corresponds to the given OpenPGP +// hash id. +func HashIdToHash(id byte) (h crypto.Hash, ok bool) { + return algorithm.HashIdToHash(id) +} + +// HashIdToString returns the name of the hash function corresponding to the +// given OpenPGP hash id. +func HashIdToString(id byte) (name string, ok bool) { + return algorithm.HashIdToString(id) +} + +// HashToHashId returns an OpenPGP hash id which corresponds the given Hash. +func HashToHashId(h crypto.Hash) (id byte, ok bool) { + return algorithm.HashToHashId(h) +} diff --git a/openpgp/integration_tests/end_to_end_test.go b/openpgp/integration_tests/end_to_end_test.go index 37d25a6a..bda40ade 100644 --- a/openpgp/integration_tests/end_to_end_test.go +++ b/openpgp/integration_tests/end_to_end_test.go @@ -308,7 +308,7 @@ func signVerifyTest( } if otherSigner.PrimaryKey.KeyId != skFrom[0].PrimaryKey.KeyId { t.Errorf( - "wrong signer got:%x want:%x", otherSigner.PrimaryKey.KeyId, 0) + "wrong signer: got %x, expected %x", otherSigner.PrimaryKey.KeyId, 0) } } @@ -333,7 +333,7 @@ func signVerifyTest( } if otherSigner.PrimaryKey.KeyId != skFrom[0].PrimaryKey.KeyId { t.Errorf( - "wrong signer got:%x want:%x", + "wrong signer: got %x, expected %x", skFrom[0].PrimaryKey.KeyId, skFrom[0].PrimaryKey.KeyId, ) diff --git a/openpgp/integration_tests/utils_test.go b/openpgp/integration_tests/utils_test.go index d08a6fb0..e1ce81fe 100644 --- a/openpgp/integration_tests/utils_test.go +++ b/openpgp/integration_tests/utils_test.go @@ -11,6 +11,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" ) // This function produces random test vectors: generates keys according to the @@ -145,7 +146,7 @@ func randFileHints() *openpgp.FileHints { return &openpgp.FileHints{ IsBinary: mathrand.Intn(2) == 0, FileName: string(fileName), - ModTime: time.Now(), + ModTime: time.Now(), } } @@ -237,9 +238,9 @@ func randConfig() *packet.Config { pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] aeadModes := []packet.AEADMode{ - packet.AEADModeEAX, packet.AEADModeOCB, - packet.AEADModeExperimentalGCM, + packet.AEADModeEAX, + packet.AEADModeGCM, } var aeadConf = packet.AEADConfig{ DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], @@ -267,6 +268,19 @@ func randConfig() *packet.Config { v5 = true } + var s2kConf *s2k.Config + if mathrand.Int()%2 == 0 { + s2kConf = &s2k.Config{ + S2KMode: s2k.IteratedSaltedS2K, + Hash: hash, + S2KCount: 1024 + mathrand.Intn(65010689), + } + } else { + s2kConf = &s2k.Config{ + S2KMode: s2k.Argon2S2K, + } + } + return &packet.Config{ V5Keys: v5, Rand: rand.Reader, @@ -274,7 +288,7 @@ func randConfig() *packet.Config { DefaultCipher: ciph, DefaultCompressionAlgo: compAlgo, CompressionConfig: compConf, - S2KCount: 1024 + mathrand.Intn(65010689), + S2KConfig: s2kConf, RSABits: rsaBits, Algorithm: pkAlgo, AEADConfig: &aeadConf, diff --git a/openpgp/internal/algorithm/aead.go b/openpgp/internal/algorithm/aead.go index 17a1bfe9..d0670651 100644 --- a/openpgp/internal/algorithm/aead.go +++ b/openpgp/internal/algorithm/aead.go @@ -16,7 +16,7 @@ type AEADMode uint8 const ( AEADModeEAX = AEADMode(1) AEADModeOCB = AEADMode(2) - AEADModeGCM = AEADMode(100) + AEADModeGCM = AEADMode(3) ) // TagLength returns the length in bytes of authentication tags. diff --git a/openpgp/internal/algorithm/hash.go b/openpgp/internal/algorithm/hash.go index f0a1815f..d1a00fc7 100644 --- a/openpgp/internal/algorithm/hash.go +++ b/openpgp/internal/algorithm/hash.go @@ -32,30 +32,25 @@ type Hash interface { // The following vars mirror the crypto/Hash supported hash functions. var ( - MD5 Hash = cryptoHash{1, crypto.MD5} - SHA1 Hash = cryptoHash{2, crypto.SHA1} - RIPEMD160 Hash = cryptoHash{3, crypto.RIPEMD160} - SHA256 Hash = cryptoHash{8, crypto.SHA256} - SHA384 Hash = cryptoHash{9, crypto.SHA384} - SHA512 Hash = cryptoHash{10, crypto.SHA512} - SHA224 Hash = cryptoHash{11, crypto.SHA224} - SHA3_256 Hash = cryptoHash{12, crypto.SHA3_256} - SHA3_512 Hash = cryptoHash{14, crypto.SHA3_512} + SHA1 Hash = cryptoHash{2, crypto.SHA1} + SHA256 Hash = cryptoHash{8, crypto.SHA256} + SHA384 Hash = cryptoHash{9, crypto.SHA384} + SHA512 Hash = cryptoHash{10, crypto.SHA512} + SHA224 Hash = cryptoHash{11, crypto.SHA224} + SHA3_256 Hash = cryptoHash{12, crypto.SHA3_256} + SHA3_512 Hash = cryptoHash{14, crypto.SHA3_512} ) // HashById represents the different hash functions specified for OpenPGP. See // http://www.iana.org/assignments/pgp-parameters/pgp-parameters.xhtml#pgp-parameters-14 var ( HashById = map[uint8]Hash{ - MD5.Id(): MD5, - SHA1.Id(): SHA1, - RIPEMD160.Id(): RIPEMD160, - SHA256.Id(): SHA256, - SHA384.Id(): SHA384, - SHA512.Id(): SHA512, - SHA224.Id(): SHA224, - SHA3_256.Id(): SHA3_256, - SHA3_512.Id(): SHA3_512, + SHA256.Id(): SHA256, + SHA384.Id(): SHA384, + SHA512.Id(): SHA512, + SHA224.Id(): SHA224, + SHA3_256.Id(): SHA3_256, + SHA3_512.Id(): SHA3_512, } ) @@ -72,15 +67,12 @@ func (h cryptoHash) Id() uint8 { } var hashNames = map[uint8]string{ - MD5.Id(): "MD5", - SHA1.Id(): "SHA1", - RIPEMD160.Id(): "RIPEMD160", - SHA256.Id(): "SHA256", - SHA384.Id(): "SHA384", - SHA512.Id(): "SHA512", - SHA224.Id(): "SHA224", - SHA3_256.Id(): "SHA3-256", - SHA3_512.Id(): "SHA3-512", + SHA256.Id(): "SHA256", + SHA384.Id(): "SHA384", + SHA512.Id(): "SHA512", + SHA224.Id(): "SHA224", + SHA3_256.Id(): "SHA3-256", + SHA3_512.Id(): "SHA3-512", } func (h cryptoHash) String() string { @@ -90,3 +82,62 @@ func (h cryptoHash) String() string { } return s } + +// HashIdToHash returns a crypto.Hash which corresponds to the given OpenPGP +// hash id. +func HashIdToHash(id byte) (h crypto.Hash, ok bool) { + if hash, ok := HashById[id]; ok { + return hash.HashFunc(), true + } + return 0, false +} + +// HashIdToHashWithSha1 returns a crypto.Hash which corresponds to the given OpenPGP +// hash id, allowing sha1. +func HashIdToHashWithSha1(id byte) (h crypto.Hash, ok bool) { + if hash, ok := HashById[id]; ok { + return hash.HashFunc(), true + } + + if id == SHA1.Id() { + return SHA1.HashFunc(), true + } + + return 0, false +} + +// HashIdToString returns the name of the hash function corresponding to the +// given OpenPGP hash id. +func HashIdToString(id byte) (name string, ok bool) { + if hash, ok := HashById[id]; ok { + return hash.String(), true + } + return "", false +} + +// HashToHashId returns an OpenPGP hash id which corresponds the given Hash. +func HashToHashId(h crypto.Hash) (id byte, ok bool) { + for id, hash := range HashById { + if hash.HashFunc() == h { + return id, true + } + } + + return 0, false +} + +// HashToHashIdWithSha1 returns an OpenPGP hash id which corresponds the given Hash, +// allowing instances of SHA1 +func HashToHashIdWithSha1(h crypto.Hash) (id byte, ok bool) { + for id, hash := range HashById { + if hash.HashFunc() == h { + return id, true + } + } + + if h == SHA1.HashFunc() { + return SHA1.Id(), true + } + + return 0, false +} diff --git a/openpgp/internal/ecc/curve25519.go b/openpgp/internal/ecc/curve25519.go index 266635ec..888767c4 100644 --- a/openpgp/internal/ecc/curve25519.go +++ b/openpgp/internal/ecc/curve25519.go @@ -9,7 +9,7 @@ import ( x25519lib "github.com/cloudflare/circl/dh/x25519" ) -type curve25519 struct {} +type curve25519 struct{} func NewCurve25519() *curve25519 { return &curve25519{} @@ -21,14 +21,14 @@ func (c *curve25519) GetCurveName() string { // MarshalBytePoint encodes the public point from native format, adding the prefix. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.6 -func (c *curve25519) MarshalBytePoint(point [] byte) []byte { +func (c *curve25519) MarshalBytePoint(point []byte) []byte { return append([]byte{0x40}, point...) } // UnmarshalBytePoint decodes the public point to native format, removing the prefix. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.6 func (c *curve25519) UnmarshalBytePoint(point []byte) []byte { - if len(point) != x25519lib.Size + 1 { + if len(point) != x25519lib.Size+1 { return nil } diff --git a/openpgp/internal/ecc/curve25519_test.go b/openpgp/internal/ecc/curve25519_test.go index 3ccb6357..f37aba56 100644 --- a/openpgp/internal/ecc/curve25519_test.go +++ b/openpgp/internal/ecc/curve25519_test.go @@ -16,13 +16,13 @@ import ( func TestGenerateMaskedPrivateKeyX25519(t *testing.T) { c := NewCurve25519() _, secret, err := c.GenerateECDH(rand.Reader) - if err != nil { + if err != nil { t.Fatal(err) } encoded := c.MarshalByteSecret(secret) decoded := c.UnmarshalByteSecret(encoded) - if decoded == nil { + if decoded == nil { t.Fatal(err) } diff --git a/openpgp/internal/ecc/curve_info.go b/openpgp/internal/ecc/curve_info.go index df2878c9..35751034 100644 --- a/openpgp/internal/ecc/curve_info.go +++ b/openpgp/internal/ecc/curve_info.go @@ -11,76 +11,76 @@ import ( type CurveInfo struct { GenName string - Oid *encoding.OID - Curve Curve + Oid *encoding.OID + Curve Curve } var Curves = []CurveInfo{ { // NIST P-256 GenName: "P256", - Oid: encoding.NewOID([]byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07}), - Curve: NewGenericCurve(elliptic.P256()), + Oid: encoding.NewOID([]byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07}), + Curve: NewGenericCurve(elliptic.P256()), }, { // NIST P-384 GenName: "P384", - Oid: encoding.NewOID([]byte{0x2B, 0x81, 0x04, 0x00, 0x22}), - Curve: NewGenericCurve(elliptic.P384()), + Oid: encoding.NewOID([]byte{0x2B, 0x81, 0x04, 0x00, 0x22}), + Curve: NewGenericCurve(elliptic.P384()), }, { // NIST P-521 GenName: "P521", - Oid: encoding.NewOID([]byte{0x2B, 0x81, 0x04, 0x00, 0x23}), - Curve: NewGenericCurve(elliptic.P521()), + Oid: encoding.NewOID([]byte{0x2B, 0x81, 0x04, 0x00, 0x23}), + Curve: NewGenericCurve(elliptic.P521()), }, { // SecP256k1 GenName: "SecP256k1", - Oid: encoding.NewOID([]byte{0x2B, 0x81, 0x04, 0x00, 0x0A}), - Curve: NewGenericCurve(bitcurves.S256()), + Oid: encoding.NewOID([]byte{0x2B, 0x81, 0x04, 0x00, 0x0A}), + Curve: NewGenericCurve(bitcurves.S256()), }, { // Curve25519 GenName: "Curve25519", - Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01}), - Curve: NewCurve25519(), + Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01}), + Curve: NewCurve25519(), }, { // X448 GenName: "Curve448", - Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x6F}), - Curve: NewX448(), + Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x6F}), + Curve: NewX448(), }, { // Ed25519 GenName: "Curve25519", - Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01}), - Curve: NewEd25519(), + Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01}), + Curve: NewEd25519(), }, { // Ed448 GenName: "Curve448", - Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x71}), - Curve: NewEd448(), + Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x71}), + Curve: NewEd448(), }, { // BrainpoolP256r1 GenName: "BrainpoolP256", - Oid: encoding.NewOID([]byte{0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07}), - Curve: NewGenericCurve(brainpool.P256r1()), + Oid: encoding.NewOID([]byte{0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07}), + Curve: NewGenericCurve(brainpool.P256r1()), }, { // BrainpoolP384r1 GenName: "BrainpoolP384", - Oid: encoding.NewOID([]byte{0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B}), - Curve: NewGenericCurve(brainpool.P384r1()), + Oid: encoding.NewOID([]byte{0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B}), + Curve: NewGenericCurve(brainpool.P384r1()), }, { // BrainpoolP512r1 GenName: "BrainpoolP512", - Oid: encoding.NewOID([]byte{0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0D}), - Curve: NewGenericCurve(brainpool.P512r1()), + Oid: encoding.NewOID([]byte{0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0D}), + Curve: NewGenericCurve(brainpool.P512r1()), }, } diff --git a/openpgp/internal/ecc/curves.go b/openpgp/internal/ecc/curves.go index c47072b4..5ed9c93b 100644 --- a/openpgp/internal/ecc/curves.go +++ b/openpgp/internal/ecc/curves.go @@ -38,7 +38,7 @@ type EdDSACurve interface { type ECDHCurve interface { Curve MarshalBytePoint([]byte) (encoded []byte) - UnmarshalBytePoint(encoded []byte) ([]byte) + UnmarshalBytePoint(encoded []byte) []byte MarshalByteSecret(d []byte) []byte UnmarshalByteSecret(d []byte) []byte GenerateECDH(rand io.Reader) (point []byte, secret []byte, err error) diff --git a/openpgp/internal/ecc/ed25519.go b/openpgp/internal/ecc/ed25519.go index 29f6cba9..54a08a8a 100644 --- a/openpgp/internal/ecc/ed25519.go +++ b/openpgp/internal/ecc/ed25519.go @@ -10,7 +10,8 @@ import ( ) const ed25519Size = 32 -type ed25519 struct {} + +type ed25519 struct{} func NewEd25519() *ed25519 { return &ed25519{} @@ -29,7 +30,7 @@ func (c *ed25519) MarshalBytePoint(x []byte) []byte { // UnmarshalBytePoint decodes a point from prefixed format to native. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.5 func (c *ed25519) UnmarshalBytePoint(point []byte) (x []byte) { - if len(point) != ed25519lib.PublicKeySize + 1 { + if len(point) != ed25519lib.PublicKeySize+1 { return nil } @@ -52,7 +53,7 @@ func (c *ed25519) UnmarshalByteSecret(s []byte) (d []byte) { // Handle stripped leading zeroes d = make([]byte, ed25519lib.SeedSize) - copy(d[ed25519lib.SeedSize - len(s):], s) + copy(d[ed25519lib.SeedSize-len(s):], s) return } diff --git a/openpgp/internal/ecc/ed448.go b/openpgp/internal/ecc/ed448.go index a2df3dab..18cd8043 100644 --- a/openpgp/internal/ecc/ed448.go +++ b/openpgp/internal/ecc/ed448.go @@ -9,7 +9,7 @@ import ( ed448lib "github.com/cloudflare/circl/sign/ed448" ) -type ed448 struct {} +type ed448 struct{} func NewEd448() *ed448 { return &ed448{} @@ -29,7 +29,7 @@ func (c *ed448) MarshalBytePoint(x []byte) []byte { // UnmarshalBytePoint decodes a point from prefixed format to native. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.5 func (c *ed448) UnmarshalBytePoint(point []byte) (x []byte) { - if len(point) != ed448lib.PublicKeySize + 1 { + if len(point) != ed448lib.PublicKeySize+1 { return nil } @@ -48,7 +48,7 @@ func (c *ed448) MarshalByteSecret(d []byte) []byte { // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.5 func (c *ed448) UnmarshalByteSecret(s []byte) (d []byte) { // Check prefixed size - if len(s) != ed448lib.SeedSize + 1 { + if len(s) != ed448lib.SeedSize+1 { return nil } @@ -66,7 +66,7 @@ func (c *ed448) MarshalSignature(sig []byte) (r, s []byte) { // UnmarshalSignature decodes R and S in the native format. Only R is used, in prefixed native format. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.2.3.3.2 func (c *ed448) UnmarshalSignature(r, s []byte) (sig []byte) { - if len(r) != ed448lib.SignatureSize + 1 { + if len(r) != ed448lib.SignatureSize+1 { return nil } diff --git a/openpgp/internal/ecc/x448.go b/openpgp/internal/ecc/x448.go index 4a940b4f..ffdd5151 100644 --- a/openpgp/internal/ecc/x448.go +++ b/openpgp/internal/ecc/x448.go @@ -9,7 +9,7 @@ import ( x448lib "github.com/cloudflare/circl/dh/x448" ) -type x448 struct {} +type x448 struct{} func NewX448() *x448 { return &x448{} @@ -28,7 +28,7 @@ func (c *x448) MarshalBytePoint(point []byte) []byte { // UnmarshalBytePoint decodes a point from prefixed format to native. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.6 func (c *x448) UnmarshalBytePoint(point []byte) []byte { - if len(point) != x448lib.Size + 1 { + if len(point) != x448lib.Size+1 { return nil } @@ -44,7 +44,7 @@ func (c *x448) MarshalByteSecret(d []byte) []byte { // UnmarshalByteSecret decodes a scalar from prefixed format to native. // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-06#section-5.5.5.6.1.2 func (c *x448) UnmarshalByteSecret(d []byte) []byte { - if len(d) != x448lib.Size + 1 { + if len(d) != x448lib.Size+1 { return nil } diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 118dd656..0e71934c 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -82,27 +82,24 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c isPrimaryId := len(t.Identities) == 0 - selfSignature := &packet.Signature{ - Version: primary.PublicKey.Version, - SigType: packet.SigTypePositiveCert, - PubKeyAlgo: primary.PublicKey.PubKeyAlgo, - Hash: config.Hash(), - CreationTime: creationTime, - KeyLifetimeSecs: &keyLifetimeSecs, - IssuerKeyId: &primary.PublicKey.KeyId, - IssuerFingerprint: primary.PublicKey.Fingerprint, - IsPrimaryId: &isPrimaryId, - FlagsValid: true, - FlagSign: true, - FlagCertify: true, - MDC: true, // true by default, see 5.8 vs. 5.14 - AEAD: config.AEAD() != nil, - V5Keys: config != nil && config.V5Keys, - } + selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config) + selfSignature.CreationTime = creationTime + selfSignature.KeyLifetimeSecs = &keyLifetimeSecs + selfSignature.IsPrimaryId = &isPrimaryId + selfSignature.FlagsValid = true + selfSignature.FlagSign = true + selfSignature.FlagCertify = true + selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14 + selfSignature.SEIPDv2 = config.AEAD() != nil // Set the PreferredHash for the SelfSignature from the packet.Config. // If it is not the must-implement algorithm from rfc4880bis, append that. - selfSignature.PreferredHash = []uint8{hashToHashId(config.Hash())} + hash, ok := algorithm.HashToHashId(config.Hash()) + if !ok { + return errors.UnsupportedError("unsupported preferred hash function") + } + + selfSignature.PreferredHash = []uint8{hash} if config.Hash() != crypto.SHA256 { selfSignature.PreferredHash = append(selfSignature.PreferredHash, hashToHashId(crypto.SHA256)) } @@ -123,9 +120,16 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c } // And for DefaultMode. - selfSignature.PreferredAEAD = []uint8{uint8(config.AEAD().Mode())} - if config.AEAD().Mode() != packet.AEADModeEAX { - selfSignature.PreferredAEAD = append(selfSignature.PreferredAEAD, uint8(packet.AEADModeEAX)) + 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 _, cipher := range selfSignature.PreferredSymmetric { + for _, mode := range modes { + selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode}) + } } // User ID binding signature @@ -153,42 +157,30 @@ func (e *Entity) AddSigningSubkey(config *packet.Config) error { return err } sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw) + sub.IsSubkey = true + if config != nil && config.V5Keys { + sub.UpgradeToV5() + } subkey := Subkey{ PublicKey: &sub.PublicKey, PrivateKey: sub, - Sig: &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: creationTime, - KeyLifetimeSecs: &keyLifetimeSecs, - SigType: packet.SigTypeSubkeyBinding, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - FlagsValid: true, - FlagSign: true, - IssuerKeyId: &e.PrimaryKey.KeyId, - EmbeddedSignature: &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: creationTime, - SigType: packet.SigTypePrimaryKeyBinding, - PubKeyAlgo: sub.PublicKey.PubKeyAlgo, - Hash: config.Hash(), - IssuerKeyId: &e.PrimaryKey.KeyId, - }, - }, - } - if config != nil && config.V5Keys { - subkey.PublicKey.UpgradeToV5() } + subkey.Sig = createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyBinding, config) + subkey.Sig.CreationTime = creationTime + subkey.Sig.KeyLifetimeSecs = &keyLifetimeSecs + subkey.Sig.FlagsValid = true + subkey.Sig.FlagSign = true + subkey.Sig.EmbeddedSignature = createSignaturePacket(subkey.PublicKey, packet.SigTypePrimaryKeyBinding, config) + subkey.Sig.EmbeddedSignature.CreationTime = creationTime err = subkey.Sig.EmbeddedSignature.CrossSignKey(subkey.PublicKey, e.PrimaryKey, subkey.PrivateKey, config) if err != nil { return err } - subkey.PublicKey.IsSubkey = true - subkey.PrivateKey.IsSubkey = true - if err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config); err != nil { + err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config) + if err != nil { return err } @@ -210,30 +202,24 @@ func (e *Entity) addEncryptionSubkey(config *packet.Config, creationTime time.Ti return err } sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw) + sub.IsSubkey = true + if config != nil && config.V5Keys { + sub.UpgradeToV5() + } subkey := Subkey{ PublicKey: &sub.PublicKey, PrivateKey: sub, - Sig: &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: creationTime, - KeyLifetimeSecs: &keyLifetimeSecs, - SigType: packet.SigTypeSubkeyBinding, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - FlagsValid: true, - FlagEncryptStorage: true, - FlagEncryptCommunications: true, - IssuerKeyId: &e.PrimaryKey.KeyId, - }, } - if config != nil && config.V5Keys { - subkey.PublicKey.UpgradeToV5() - } - - subkey.PublicKey.IsSubkey = true - subkey.PrivateKey.IsSubkey = true - if err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config); err != nil { + subkey.Sig = createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyBinding, config) + subkey.Sig.CreationTime = creationTime + subkey.Sig.KeyLifetimeSecs = &keyLifetimeSecs + subkey.Sig.FlagsValid = true + subkey.Sig.FlagEncryptStorage = true + subkey.Sig.FlagEncryptCommunications = true + + err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config) + if err != nil { return err } diff --git a/openpgp/keys.go b/openpgp/keys.go index 98a293d1..fe49ab75 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -158,11 +158,9 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { return Key{e, subkey.PublicKey, subkey.PrivateKey, subkey.Sig, subkey.Revocations}, true } - // If we don't have any candidate subkeys for encryption and - // the primary key doesn't have any usage metadata then we - // assume that the primary key is ok. Or, if the primary key is - // marked as ok to encrypt with, then we can obviously use it. - if !i.SelfSignature.FlagsValid || i.SelfSignature.FlagEncryptCommunications && + // 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. + if i.SelfSignature.FlagsValid && i.SelfSignature.FlagEncryptCommunications && e.PrimaryKey.PubKeyAlgo.CanEncrypt() { return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true } @@ -228,11 +226,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, return Key{e, subkey.PublicKey, subkey.PrivateKey, subkey.Sig, subkey.Revocations}, true } - // If we have no candidate subkey then we assume that it's ok to sign - // with the primary key. Or, if the primary key is marked as ok to - // sign with, then we can use it. - if !i.SelfSignature.FlagsValid || ((flags&packet.KeyFlagCertify == 0 || i.SelfSignature.FlagCertify) && - (flags&packet.KeyFlagSign == 0 || i.SelfSignature.FlagSign)) && + + // 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. + if i.SelfSignature.FlagsValid && + (flags&packet.KeyFlagCertify == 0 || i.SelfSignature.FlagCertify) && + (flags&packet.KeyFlagSign == 0 || i.SelfSignature.FlagSign) && e.PrimaryKey.PubKeyAlgo.CanSign() && (id == 0 || e.PrimaryKey.KeyId == id) { return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true @@ -262,6 +261,44 @@ func (e *Entity) Revoked(now time.Time) bool { return revoked(e.Revocations, now) } +// EncryptPrivateKeys encrypts all non-encrypted keys in the entity with the same key +// derived from the provided passphrase. Public keys and dummy keys are ignored, +// and don't cause an error to be returned. +func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) error { + var keysToEncrypt []*packet.PrivateKey + // Add entity private key to encrypt. + if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted { + keysToEncrypt = append(keysToEncrypt, e.PrivateKey) + } + + // Add subkeys to encrypt. + for _, sub := range e.Subkeys { + if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && !sub.PrivateKey.Encrypted { + keysToEncrypt = append(keysToEncrypt, sub.PrivateKey) + } + } + return packet.EncryptPrivateKeys(keysToEncrypt, passphrase, config) +} + +// DecryptPrivateKeys decrypts all encrypted keys in the entitiy with the given passphrase. +// Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored, +// and don't cause an error to be returned. +func (e *Entity) DecryptPrivateKeys(passphrase []byte) error { + var keysToDecrypt []*packet.PrivateKey + // Add entity private key to decrypt. + if e.PrivateKey != nil && !e.PrivateKey.Dummy() && e.PrivateKey.Encrypted { + keysToDecrypt = append(keysToDecrypt, e.PrivateKey) + } + + // Add subkeys to decrypt. + for _, sub := range e.Subkeys { + if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && sub.PrivateKey.Encrypted { + keysToDecrypt = append(keysToDecrypt, sub.PrivateKey) + } + } + return packet.DecryptPrivateKeys(keysToDecrypt, passphrase) +} + // Revoked returns whether the identity has been revoked by a self-signature. // Note that third-party revocation signatures are not supported. func (i *Identity) Revoked(now time.Time) bool { @@ -309,7 +346,11 @@ func (el EntityList) KeysById(id uint64) (keys []Key) { // the bitwise-OR of packet.KeyFlag* values. func (el EntityList) KeysByIdUsage(id uint64, requiredUsage byte) (keys []Key) { for _, key := range el.KeysById(id) { - if key.SelfSignature != nil && key.SelfSignature.FlagsValid && requiredUsage != 0 { + if requiredUsage != 0 { + if key.SelfSignature == nil || !key.SelfSignature.FlagsValid { + continue + } + var usage byte if key.SelfSignature.FlagCertify { usage |= packet.KeyFlagCertify @@ -337,7 +378,7 @@ func (el EntityList) KeysByIdUsage(id uint64, requiredUsage byte) (keys []Key) { func (el EntityList) DecryptionKeys() (keys []Key) { for _, e := range el { for _, subKey := range e.Subkeys { - if subKey.PrivateKey != nil && (!subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications) { + if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications) { keys = append(keys, Key{e, subKey.PublicKey, subKey.PrivateKey, subKey.Sig, subKey.Revocations}) } } @@ -476,7 +517,7 @@ EachPacket: // Else, ignoring the signature as it does not follow anything // we would know to attach it to. case *packet.PrivateKey: - if pkt.IsSubkey == false { + if !pkt.IsSubkey { packets.Unread(p) break EachPacket } @@ -485,7 +526,7 @@ EachPacket: return nil, err } case *packet.PublicKey: - if pkt.IsSubkey == false { + if !pkt.IsSubkey { packets.Unread(p) break EachPacket } @@ -806,18 +847,7 @@ func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Co return errors.InvalidArgumentError("given identity string not found in Entity") } - sig := &packet.Signature{ - Version: certificationKey.PrivateKey.Version, - SigType: packet.SigTypeGenericCert, - PubKeyAlgo: certificationKey.PrivateKey.PubKeyAlgo, - Hash: config.Hash(), - CreationTime: config.Now(), - IssuerKeyId: &certificationKey.PrivateKey.KeyId, - } - - if config.SigLifetime() != 0 { - sig.SigLifetimeSecs = &config.SigLifetimeSecs - } + sig := createSignaturePacket(certificationKey.PublicKey, packet.SigTypeGenericCert, config) signingUserID := config.SigningUserId() if signingUserID != "" { @@ -838,16 +868,9 @@ func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Co // specified reason code and text (RFC4880 section-5.2.3.23). // If config is nil, sensible defaults will be used. func (e *Entity) RevokeKey(reason packet.ReasonForRevocation, reasonText string, config *packet.Config) error { - revSig := &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: config.Now(), - SigType: packet.SigTypeKeyRevocation, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - RevocationReason: &reason, - RevocationReasonText: reasonText, - IssuerKeyId: &e.PrimaryKey.KeyId, - } + revSig := createSignaturePacket(e.PrimaryKey, packet.SigTypeKeyRevocation, config) + revSig.RevocationReason = &reason + revSig.RevocationReasonText = reasonText if err := revSig.RevokeKey(e.PrimaryKey, e.PrivateKey, config); err != nil { return err @@ -864,16 +887,9 @@ func (e *Entity) RevokeSubkey(sk *Subkey, reason packet.ReasonForRevocation, rea return errors.InvalidArgumentError("given subkey is not associated with this key") } - revSig := &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: config.Now(), - SigType: packet.SigTypeSubkeyRevocation, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - RevocationReason: &reason, - RevocationReasonText: reasonText, - IssuerKeyId: &e.PrimaryKey.KeyId, - } + revSig := createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyRevocation, config) + revSig.RevocationReason = &reason + revSig.RevocationReasonText = reasonText if err := revSig.RevokeSubkey(sk.PublicKey, e.PrivateKey, config); err != nil { return err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 3cd2e05d..35bb495b 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -6,6 +6,7 @@ import ( "crypto/dsa" "crypto/rand" "crypto/rsa" + "fmt" "math/big" "strconv" "strings" @@ -20,6 +21,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" ) var hashes = []crypto.Hash{ @@ -41,8 +43,9 @@ var ciphers = []packet.CipherFunction{ } var aeadModes = []packet.AEADMode{ - packet.AEADModeEAX, packet.AEADModeOCB, + packet.AEADModeEAX, + packet.AEADModeGCM, } func TestKeyExpiry(t *testing.T) { @@ -120,7 +123,7 @@ func TestExpiringPrimaryUIDKey(t *testing.T) { } } -func TestReturnFirstUnexpiredSigningSubkey(t *testing.T) { +func TestReturnNewestUnexpiredSigningSubkey(t *testing.T) { // Make a master key. entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) if err != nil { @@ -137,6 +140,9 @@ func TestReturnFirstUnexpiredSigningSubkey(t *testing.T) { // Second signing subkey expires in a day. err = entity.AddSigningSubkey(&packet.Config{ + Time: func() time.Time { + return time.Now().Add(1 * time.Second) + }, KeyLifetimeSecs: 24 * 60 * 60, }) if err != nil { @@ -146,7 +152,7 @@ func TestReturnFirstUnexpiredSigningSubkey(t *testing.T) { subkey2 := entity.Subkeys[2] // Before second signing subkey has expired, it should be returned. - time1 := time.Now() + time1 := time.Now().Add(2 * time.Second) expected := subkey2.PublicKey.KeyIdShortString() subkey, found := entity.SigningKey(time1) if !found { @@ -801,6 +807,13 @@ func TestNewEntityWithDefaultHash(t *testing.T) { DefaultHash: hash, } entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if hash == crypto.SHA1 { + if err == nil { + t.Fatal("should fail on SHA1 key creation") + } + continue + } + if err != nil { t.Fatal(err) } @@ -899,14 +912,20 @@ func TestNewEntityWithDefaultAead(t *testing.T) { } for _, identity := range entity.Identities { - if len(identity.SelfSignature.PreferredAEAD) == 0 { + if len(identity.SelfSignature.PreferredCipherSuites) == 0 { t.Fatal("didn't find a preferred mode in self signature") } - mode := identity.SelfSignature.PreferredAEAD[0] + cipher := identity.SelfSignature.PreferredCipherSuites[0][0] + if cipher != uint8(cfg.Cipher()) { + t.Fatalf("Expected preferred cipher to be %d, got %d", + uint8(cfg.Cipher()), + identity.SelfSignature.PreferredCipherSuites[0][0]) + } + mode := identity.SelfSignature.PreferredCipherSuites[0][1] if mode != uint8(cfg.AEAD().DefaultMode) { t.Fatalf("Expected preferred mode to be %d, got %d", uint8(cfg.AEAD().DefaultMode), - identity.SelfSignature.PreferredAEAD[0]) + identity.SelfSignature.PreferredCipherSuites[0][1]) } } } @@ -946,6 +965,69 @@ func TestNewEntityPrivateSerialization(t *testing.T) { } } +func TestNotationPacket(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithNotation)) + if err != nil { + t.Fatal(err) + } + + assertNotationPackets(t, keys) + + serializedEntity := bytes.NewBuffer(nil) + err = keys[0].SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + + keys, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + + assertNotationPackets(t, keys) +} + +func assertNotationPackets(t *testing.T, keys EntityList) { + if len(keys) != 1 { + t.Errorf("Failed to accept key, %d", len(keys)) + } + + identity := keys[0].Identities["Test "] + + if numSigs, numExpected := len(identity.Signatures), 1; numSigs != numExpected { + t.Fatalf("got %d signatures, expected %d", numSigs, numExpected) + } + + notations := identity.Signatures[0].Notations + if numNotations, numExpected := len(notations), 2; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + + if notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + + if notations[0].Name != "text@example.com" { + t.Fatalf("got %s, expected text@example.com", notations[0].Name) + } + + if string(notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(notations[0].Value)) + } + + if notations[1].IsHumanReadable != false { + t.Fatalf("got true, expected false") + } + + if notations[1].Name != "binary@example.com" { + t.Fatalf("got %s, expected binary@example.com", notations[1].Name) + } + + if !bytes.Equal(notations[1].Value, []byte{0, 1, 2, 3}) { + t.Fatalf("got %s, expected {0, 1, 2, 3}", string(notations[1].Value)) + } +} + func TestEntityPrivateSerialization(t *testing.T) { keys, err := ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKeyBlock)) if err != nil { @@ -1378,6 +1460,64 @@ func TestRevokeSubkeyWithConfig(t *testing.T) { } } +func TestEncryptAndDecryptPrivateKeys(t *testing.T) { + s2kModesToTest := []s2k.Mode{s2k.IteratedSaltedS2K, s2k.Argon2S2K} + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(nil) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(nil) + if err != nil { + t.Fatal(err) + } + for _, mode := range s2kModesToTest { + t.Run(fmt.Sprintf("S2KMode %d", mode), func(t *testing.T) { + passphrase := []byte("password") + config := &packet.Config{ + S2KConfig: &s2k.Config{ + S2KMode: mode, + }, + } + err = entity.EncryptPrivateKeys(passphrase, config) + if err != nil { + t.Fatal(err) + } + + if !entity.PrivateKey.Encrypted { + t.Fatal("Expected encrypted private key") + } + for _, subkey := range entity.Subkeys { + if !subkey.PrivateKey.Encrypted { + t.Fatal("Expected encrypted private key") + } + } + + err = entity.DecryptPrivateKeys(passphrase) + if err != nil { + t.Fatal(err) + } + + if entity.PrivateKey.Encrypted { + t.Fatal("Expected plaintext private key") + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey.Encrypted { + t.Fatal("Expected plaintext private key") + } + } + }) + } + + +} + func TestKeyValidateOnDecrypt(t *testing.T) { randomPassword := make([]byte, 128) _, err := rand.Read(randomPassword) @@ -1400,7 +1540,7 @@ func TestKeyValidateOnDecrypt(t *testing.T) { }) for _, bits := range []int{2048, 3072, 4096} { - t.Run("Generated:" + strconv.Itoa(bits) + " bits", func(t *testing.T) { + t.Run("Generated:"+strconv.Itoa(bits)+" bits", func(t *testing.T) { key := testGenerateRSA(t, bits) testKeyValidateRsaOnDecrypt(t, key, randomPassword) }) @@ -1425,18 +1565,18 @@ func TestKeyValidateOnDecrypt(t *testing.T) { testKeyValidateEcdsaOnDecrypt(t, keys[0], randomPassword) }) - ecdsaCurves := map[string] packet.Curve { - "NIST P-256": packet.CurveNistP256, - "NIST P-384": packet.CurveNistP384, - "NIST P-521": packet.CurveNistP521, + ecdsaCurves := map[string]packet.Curve{ + "NIST P-256": packet.CurveNistP256, + "NIST P-384": packet.CurveNistP384, + "NIST P-521": packet.CurveNistP521, "Brainpool P-256": packet.CurveBrainpoolP256, "Brainpool P-384": packet.CurveBrainpoolP384, "Brainpool P-512": packet.CurveBrainpoolP512, - "SecP256k1": packet.CurveSecP256k1, + "SecP256k1": packet.CurveSecP256k1, } for name, curveType := range ecdsaCurves { - t.Run("Generated:" + name, func(t *testing.T) { + t.Run("Generated:"+name, func(t *testing.T) { key := testGenerateEC(t, packet.PubKeyAlgoECDSA, curveType) testKeyValidateEcdsaOnDecrypt(t, key, randomPassword) }) @@ -1444,13 +1584,13 @@ func TestKeyValidateOnDecrypt(t *testing.T) { }) t.Run("EdDSA", func(t *testing.T) { - eddsaHardcoded := map[string] string { + eddsaHardcoded := map[string]string{ "Curve25519": curve25519PrivateKey, - "Curve448": curve448PrivateKey, + "Curve448": curve448PrivateKey, } for name, skData := range eddsaHardcoded { - t.Run("Hardcoded:" + name, func(t *testing.T) { + t.Run("Hardcoded:"+name, func(t *testing.T) { keys, err := ReadArmoredKeyRing(bytes.NewBufferString(skData)) if err != nil { t.Fatal("Unable to parse hardcoded key: ", err) @@ -1460,13 +1600,13 @@ func TestKeyValidateOnDecrypt(t *testing.T) { }) } - eddsaCurves := map[string] packet.Curve { + eddsaCurves := map[string]packet.Curve{ "Curve25519": packet.Curve25519, - "Curve448": packet.Curve448, + "Curve448": packet.Curve448, } for name, curveType := range eddsaCurves { - t.Run("Generated:" + name, func(t *testing.T) { + t.Run("Generated:"+name, func(t *testing.T) { key := testGenerateEC(t, packet.PubKeyAlgoEdDSA, curveType) testKeyValidateEddsaOnDecrypt(t, key, randomPassword) }) @@ -1479,7 +1619,7 @@ func TestKeyValidateOnDecrypt(t *testing.T) { } func testGenerateRSA(t *testing.T, bits int) *Entity { - config := &packet.Config{Algorithm:packet.PubKeyAlgoRSA, RSABits: bits} + config := &packet.Config{Algorithm: packet.PubKeyAlgoRSA, RSABits: bits} rsaEntity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) if err != nil { t.Fatal(err) @@ -1592,7 +1732,7 @@ func testKeyValidateEddsaOnDecrypt(t *testing.T, eddsaEntity *Entity, password [ } // ECDH - ecdhSubkey := eddsaEntity.Subkeys[len(eddsaEntity.Subkeys) - 1].PrivateKey + ecdhSubkey := eddsaEntity.Subkeys[len(eddsaEntity.Subkeys)-1].PrivateKey if err = ecdhSubkey.Encrypt(password); err != nil { t.Fatal(err) } diff --git a/openpgp/keys_test_data.go b/openpgp/keys_test_data.go index 4bcfd5fd..108fd096 100644 --- a/openpgp/keys_test_data.go +++ b/openpgp/keys_test_data.go @@ -518,3 +518,21 @@ XLCBln+wdewpU4ChEffMUDRBfqfQco/YsMqWV7bHJHAO0eC/DMKCjyU90xdH7R/d QgqsfguR1PqPuJxpXV4bSr6CGAAAAA== =MSvh -----END PGP PRIVATE KEY BLOCK-----` + +const keyWithNotation = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEY9gIshYJKwYBBAHaRw8BAQdAF25fSM8OpFlXZhop4Qpqo5ywGZ4jgWlR +ppjhIKDthREAAQC+LFpzFcMJYcjxGKzBGHN0Px2jU4d04YSRnFAik+lVVQ6u +zRdUZXN0IDx0ZXN0QGV4YW1wbGUuY29tPsLACgQQFgoAfAUCY9gIsgQLCQcI +CRD/utJOCym8pR0UgAAAAAAQAAR0ZXh0QGV4YW1wbGUuY29tdGVzdB8UAAAA +AAASAARiaW5hcnlAZXhhbXBsZS5jb20AAQIDAxUICgQWAAIBAhkBAhsDAh4B +FiEEEMCQTUVGKgCX5rDQ/7rSTgspvKUAAPl5AP9Npz90LxzrB97Qr2DrGwfG +wuYn4FSYwtuPfZHHeoIabwD/QEbvpQJ/NBb9EAZuow4Rirlt1yv19mmnF+j5 +8yUzhQjHXQRj2AiyEgorBgEEAZdVAQUBAQdARXAo30DmKcyUg6co7OUm0RNT +z9iqFbDBzA8A47JEt1MDAQgHAAD/XKK3lBm0SqMR558HLWdBrNG6NqKuqb5X +joCML987ZNgRD8J4BBgWCAAqBQJj2AiyCRD/utJOCym8pQIbDBYhBBDAkE1F +RioAl+aw0P+60k4LKbylAADRxgEAg7UfBDiDPp5LHcW9D+SgFHk6+GyEU4ev +VppQxdtxPvAA/34snHBX7Twnip1nMt7P4e2hDiw/hwQ7oqioOvc6jMkP +=Z8YJ +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/keys_v5_test.go b/openpgp/keys_v5_test.go index 1ea70de3..8a38bd90 100644 --- a/openpgp/keys_v5_test.go +++ b/openpgp/keys_v5_test.go @@ -137,12 +137,12 @@ func TestNewEntityV5Key(t *testing.T) { func checkV5Key(t *testing.T, ent *Entity) { key := ent.PrimaryKey - if key.Version != 5 { - t.Errorf("wrong key version %d", key.Version) - } - if len(key.Fingerprint) != 32 { - t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) - } + if key.Version != 5 { + t.Errorf("wrong key version %d", key.Version) + } + if len(key.Fingerprint) != 32 { + t.Errorf("Wrong fingerprint length: %d", len(key.Fingerprint)) + } signatures := ent.Revocations for _, id := range ent.Identities { signatures = append(signatures, id.SelfSignature) @@ -156,7 +156,7 @@ func checkV5Key(t *testing.T, ent *Entity) { t.Errorf("wrong signature version %d", sig.Version) } fgptLen := len(sig.IssuerFingerprint) - if fgptLen!= 32 { + if fgptLen != 32 { t.Errorf("Wrong fingerprint length in signature: %d", fgptLen) } } diff --git a/openpgp/packet/aead_config.go b/openpgp/packet/aead_config.go index 7350974e..fec41a0e 100644 --- a/openpgp/packet/aead_config.go +++ b/openpgp/packet/aead_config.go @@ -4,6 +4,14 @@ package packet import "math/bits" +// CipherSuite contains a combination of Cipher and Mode +type CipherSuite struct { + // The cipher function + Cipher CipherFunction + // The AEAD mode of operation. + Mode AEADMode +} + // AEADConfig collects a number of AEAD parameters along with sensible defaults. // A nil AEADConfig is valid and results in all default values. type AEADConfig struct { @@ -15,12 +23,13 @@ type AEADConfig struct { // Mode returns the AEAD mode of operation. func (conf *AEADConfig) Mode() AEADMode { + // If no preference is specified, OCB is used (which is mandatory to implement). if conf == nil || conf.DefaultMode == 0 { - return AEADModeEAX + return AEADModeOCB } + mode := conf.DefaultMode - if mode != AEADModeEAX && mode != AEADModeOCB && - mode != AEADModeExperimentalGCM { + if mode != AEADModeEAX && mode != AEADModeOCB && mode != AEADModeGCM { panic("AEAD mode unsupported") } return mode @@ -28,6 +37,8 @@ func (conf *AEADConfig) Mode() AEADMode { // ChunkSizeByte returns the byte indicating the chunk size. The effective // chunk size is computed with the formula uint64(1) << (chunkSizeByte + 6) +// limit to 16 = 4 MiB +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2 func (conf *AEADConfig) ChunkSizeByte() byte { if conf == nil || conf.ChunkSize == 0 { return 12 // 1 << (12 + 6) == 262144 bytes @@ -38,8 +49,8 @@ func (conf *AEADConfig) ChunkSizeByte() byte { switch { case exponent < 6: exponent = 6 - case exponent > 27: - exponent = 27 + case exponent > 16: + exponent = 16 } return byte(exponent - 6) diff --git a/openpgp/packet/aead_crypter.go b/openpgp/packet/aead_crypter.go new file mode 100644 index 00000000..cee83bdc --- /dev/null +++ b/openpgp/packet/aead_crypter.go @@ -0,0 +1,264 @@ +// Copyright (C) 2019 ProtonTech AG + +package packet + +import ( + "bytes" + "crypto/cipher" + "encoding/binary" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +// aeadCrypter is an AEAD opener/sealer, its configuration, and data for en/decryption. +type aeadCrypter struct { + aead cipher.AEAD + chunkSize int + initialNonce []byte + associatedData []byte // Chunk-independent associated data + chunkIndex []byte // Chunk counter + packetTag packetType // SEIP packet (v2) or AEAD Encrypted Data packet + bytesProcessed int // Amount of plaintext bytes encrypted/decrypted + buffer bytes.Buffer // Buffered bytes across chunks +} + +// computeNonce takes the incremental index and computes an eXclusive OR with +// the least significant 8 bytes of the receivers' initial nonce (see sec. +// 5.16.1 and 5.16.2). It returns the resulting nonce. +func (wo *aeadCrypter) computeNextNonce() (nonce []byte) { + if wo.packetTag == packetTypeSymmetricallyEncryptedIntegrityProtected { + return append(wo.initialNonce, wo.chunkIndex...) + } + + nonce = make([]byte, len(wo.initialNonce)) + copy(nonce, wo.initialNonce) + offset := len(wo.initialNonce) - 8 + for i := 0; i < 8; i++ { + nonce[i+offset] ^= wo.chunkIndex[i] + } + return +} + +// incrementIndex performs an integer increment by 1 of the integer represented by the +// slice, modifying it accordingly. +func (wo *aeadCrypter) incrementIndex() error { + index := wo.chunkIndex + if len(index) == 0 { + return errors.AEADError("Index has length 0") + } + for i := len(index) - 1; i >= 0; i-- { + if index[i] < 255 { + index[i]++ + return nil + } + index[i] = 0 + } + return errors.AEADError("cannot further increment index") +} + +// aeadDecrypter reads and decrypts bytes. It buffers extra decrypted bytes when +// necessary, similar to aeadEncrypter. +type aeadDecrypter struct { + aeadCrypter // Embedded ciphertext opener + reader io.Reader // 'reader' is a partialLengthReader + peekedBytes []byte // Used to detect last chunk + eof bool +} + +// Read decrypts bytes and reads them into dst. It decrypts when necessary and +// buffers extra decrypted bytes. It returns the number of bytes copied into dst +// and an error. +func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { + // Return buffered plaintext bytes from previous calls + if ar.buffer.Len() > 0 { + return ar.buffer.Read(dst) + } + + // Return EOF if we've previously validated the final tag + if ar.eof { + return 0, io.EOF + } + + // Read a chunk + tagLen := ar.aead.Overhead() + cipherChunkBuf := new(bytes.Buffer) + _, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize+tagLen)) + cipherChunk := cipherChunkBuf.Bytes() + if errRead != nil && errRead != io.EOF { + return 0, errRead + } + decrypted, errChunk := ar.openChunk(cipherChunk) + if errChunk != nil { + return 0, errChunk + } + + // 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 + if errRead == io.EOF { + errChunk := ar.validateFinalTag(ar.peekedBytes) + if errChunk != nil { + return n, errChunk + } + ar.eof = true // Mark EOF for when we've returned all buffered data + } + return +} + +// Close is noOp. The final authentication tag of the stream was already +// checked in the last Read call. In the future, this function could be used to +// wipe the reader and peeked, decrypted bytes, if necessary. +func (ar *aeadDecrypter) Close() (err error) { + return nil +} + +// openChunk decrypts and checks integrity of an encrypted chunk, returning +// the underlying plaintext and an error. It accesses peeked bytes from next +// chunk, to identify the last chunk and decrypt/validate accordingly. +func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) { + tagLen := ar.aead.Overhead() + // Restore carried bytes from last call + chunkExtra := append(ar.peekedBytes, data...) + // 'chunk' contains encrypted bytes, followed by an authentication tag. + chunk := chunkExtra[:len(chunkExtra)-tagLen] + ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:] + + adata := ar.associatedData + if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted { + adata = append(ar.associatedData, ar.chunkIndex...) + } + + nonce := ar.computeNextNonce() + plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata) + if err != nil { + return nil, err + } + ar.bytesProcessed += len(plainChunk) + if err = ar.aeadCrypter.incrementIndex(); err != nil { + return nil, err + } + return plainChunk, nil +} + +// Checks the summary tag. It takes into account the total decrypted bytes into +// the associated data. It returns an error, or nil if the tag is valid. +func (ar *aeadDecrypter) validateFinalTag(tag []byte) error { + // Associated: tag, version, cipher, aead, chunk size, ... + amountBytes := make([]byte, 8) + binary.BigEndian.PutUint64(amountBytes, uint64(ar.bytesProcessed)) + + adata := ar.associatedData + if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted { + // ... index ... + adata = append(ar.associatedData, ar.chunkIndex...) + } + + // ... and total number of encrypted octets + adata = append(adata, amountBytes...) + nonce := ar.computeNextNonce() + _, err := ar.aead.Open(nil, nonce, tag, adata) + if err != nil { + return err + } + return nil +} + +// aeadEncrypter encrypts and writes bytes. It encrypts when necessary according +// to the AEAD block size, and buffers the extra encrypted bytes for next write. +type aeadEncrypter struct { + aeadCrypter // Embedded plaintext sealer + writer io.WriteCloser // 'writer' is a partialLengthWriter +} + +// Write encrypts and writes bytes. It encrypts when necessary and buffers extra +// plaintext bytes for next call. When the stream is finished, Close() MUST be +// called to append the final tag. +func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) { + // Append plaintextBytes to existing buffered bytes + n, err = aw.buffer.Write(plaintextBytes) + if err != nil { + return n, err + } + // Encrypt and write chunks + for aw.buffer.Len() >= aw.chunkSize { + plainChunk := aw.buffer.Next(aw.chunkSize) + encryptedChunk, err := aw.sealChunk(plainChunk) + if err != nil { + return n, err + } + _, err = aw.writer.Write(encryptedChunk) + if err != nil { + return n, err + } + } + return +} + +// Close encrypts and writes the remaining buffered plaintext if any, appends +// the final authentication tag, and closes the embedded writer. This function +// MUST be called at the end of a stream. +func (aw *aeadEncrypter) Close() (err error) { + // Encrypt and write a chunk if there's buffered data left, or if we haven't + // written any chunks yet. + if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 { + plainChunk := aw.buffer.Bytes() + lastEncryptedChunk, err := aw.sealChunk(plainChunk) + if err != nil { + return err + } + _, err = aw.writer.Write(lastEncryptedChunk) + if err != nil { + return err + } + } + // Compute final tag (associated data: packet tag, version, cipher, aead, + // chunk size... + adata := aw.associatedData + + if aw.aeadCrypter.packetTag == packetTypeAEADEncrypted { + // ... index ... + adata = append(aw.associatedData, aw.chunkIndex...) + } + + // ... and total number of encrypted octets + amountBytes := make([]byte, 8) + binary.BigEndian.PutUint64(amountBytes, uint64(aw.bytesProcessed)) + adata = append(adata, amountBytes...) + + nonce := aw.computeNextNonce() + finalTag := aw.aead.Seal(nil, nonce, nil, adata) + _, err = aw.writer.Write(finalTag) + if err != nil { + return err + } + return aw.writer.Close() +} + +// sealChunk Encrypts and authenticates the given chunk. +func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) { + if len(data) > aw.chunkSize { + return nil, errors.AEADError("chunk exceeds maximum length") + } + if aw.associatedData == nil { + return nil, errors.AEADError("can't seal without headers") + } + adata := aw.associatedData + if aw.aeadCrypter.packetTag == packetTypeAEADEncrypted { + adata = append(aw.associatedData, aw.chunkIndex...) + } + + nonce := aw.computeNextNonce() + encrypted := aw.aead.Seal(nil, nonce, data, adata) + aw.bytesProcessed += len(data) + if err := aw.aeadCrypter.incrementIndex(); err != nil { + return nil, err + } + return encrypted, nil +} diff --git a/openpgp/packet/aead_encrypted.go b/openpgp/packet/aead_encrypted.go index 862b1ac0..98bd876b 100644 --- a/openpgp/packet/aead_encrypted.go +++ b/openpgp/packet/aead_encrypted.go @@ -3,17 +3,14 @@ package packet import ( - "bytes" - "crypto/cipher" - "crypto/rand" - "encoding/binary" "io" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" ) -// AEADEncrypted represents an AEAD Encrypted Packet (tag 20, RFC4880bis-5.16). +// AEADEncrypted represents an AEAD Encrypted Packet. +// See https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-aead-encrypted-data-packet-t type AEADEncrypted struct { cipher CipherFunction mode AEADMode @@ -25,33 +22,6 @@ type AEADEncrypted struct { // Only currently defined version const aeadEncryptedVersion = 1 -// An AEAD opener/sealer, its configuration, and data for en/decryption. -type aeadCrypter struct { - aead cipher.AEAD - chunkSize int - initialNonce []byte - associatedData []byte // Chunk-independent associated data - chunkIndex []byte // Chunk counter - bytesProcessed int // Amount of plaintext bytes encrypted/decrypted - buffer bytes.Buffer // Buffered bytes across chunks -} - -// aeadEncrypter encrypts and writes bytes. It encrypts when necessary according -// to the AEAD block size, and buffers the extra encrypted bytes for next write. -type aeadEncrypter struct { - aeadCrypter // Embedded plaintext sealer - writer io.WriteCloser // 'writer' is a partialLengthWriter -} - -// aeadDecrypter reads and decrypts bytes. It buffers extra decrypted bytes when -// necessary, similar to aeadEncrypter. -type aeadDecrypter struct { - aeadCrypter // Embedded ciphertext opener - reader io.Reader // 'reader' is a partialLengthReader - peekedBytes []byte // Used to detect last chunk - eof bool -} - func (ae *AEADEncrypted) parse(buf io.Reader) error { headerData := make([]byte, 4) if n, err := io.ReadFull(buf, headerData); n < 4 { @@ -59,10 +29,14 @@ func (ae *AEADEncrypted) parse(buf io.Reader) error { } // Read initial nonce mode := AEADMode(headerData[2]) - nonceLen := mode.NonceLength() - if nonceLen == 0 { + nonceLen := mode.IvLength() + + // This packet supports only EAX and OCB + // https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-aead-encrypted-data-packet-t + if nonceLen == 0 || mode > AEADModeOCB { return errors.AEADError("unknown mode") } + initialNonce := make([]byte, nonceLen) if n, err := io.ReadFull(buf, initialNonce); n < nonceLen { return errors.AEADError("could not read aead nonce:" + err.Error()) @@ -75,7 +49,7 @@ func (ae *AEADEncrypted) parse(buf io.Reader) error { } ae.cipher = CipherFunction(c) ae.mode = mode - ae.chunkSizeByte = byte(headerData[3]) + ae.chunkSizeByte = headerData[3] return nil } @@ -105,225 +79,13 @@ func (ae *AEADEncrypted) decrypt(key []byte) (io.ReadCloser, error) { initialNonce: ae.initialNonce, associatedData: ae.associatedData(), chunkIndex: make([]byte, 8), + packetTag: packetTypeAEADEncrypted, }, reader: ae.Contents, peekedBytes: peekedBytes}, nil } -// Read decrypts bytes and reads them into dst. It decrypts when necessary and -// buffers extra decrypted bytes. It returns the number of bytes copied into dst -// and an error. -func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { - // Return buffered plaintext bytes from previous calls - if ar.buffer.Len() > 0 { - return ar.buffer.Read(dst) - } - - // Return EOF if we've previously validated the final tag - if ar.eof { - return 0, io.EOF - } - - // Read a chunk - tagLen := ar.aead.Overhead() - cipherChunkBuf := new(bytes.Buffer) - _, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize + tagLen)) - cipherChunk := cipherChunkBuf.Bytes() - if errRead != nil && errRead != io.EOF { - return 0, errRead - } - decrypted, errChunk := ar.openChunk(cipherChunk) - if errChunk != nil { - return 0, errChunk - } - - // 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 - if errRead == io.EOF { - errChunk := ar.validateFinalTag(ar.peekedBytes) - if errChunk != nil { - return n, errChunk - } - ar.eof = true // Mark EOF for when we've returned all buffered data - } - return -} - -// Close is noOp. The final authentication tag of the stream was already -// checked in the last Read call. In the future, this function could be used to -// wipe the reader and peeked, decrypted bytes, if necessary. -func (ar *aeadDecrypter) Close() (err error) { - return nil -} - -// SerializeAEADEncrypted initializes the aeadCrypter and returns a writer. -// This writer encrypts and writes bytes (see aeadEncrypter.Write()). -func SerializeAEADEncrypted(w io.Writer, key []byte, cipher CipherFunction, mode AEADMode, config *Config) (io.WriteCloser, error) { - writeCloser := noOpCloser{w} - writer, err := serializeStreamHeader(writeCloser, packetTypeAEADEncrypted) - if err != nil { - return nil, err - } - - // Data for en/decryption: tag, version, cipher, aead mode, chunk size - aeadConf := config.AEAD() - prefix := []byte{ - 0xD4, - aeadEncryptedVersion, - byte(config.Cipher()), - byte(aeadConf.Mode()), - aeadConf.ChunkSizeByte(), - } - n, err := writer.Write(prefix[1:]) - if err != nil || n < 4 { - return nil, errors.AEADError("could not write AEAD headers") - } - // Sample nonce - nonceLen := aeadConf.Mode().NonceLength() - nonce := make([]byte, nonceLen) - n, err = rand.Read(nonce) - if err != nil { - panic("Could not sample random nonce") - } - _, err = writer.Write(nonce) - if err != nil { - return nil, err - } - blockCipher := CipherFunction(config.Cipher()).new(key) - alg := AEADMode(aeadConf.Mode()).new(blockCipher) - - chunkSize := decodeAEADChunkSize(aeadConf.ChunkSizeByte()) - return &aeadEncrypter{ - aeadCrypter: aeadCrypter{ - aead: alg, - chunkSize: chunkSize, - associatedData: prefix, - chunkIndex: make([]byte, 8), - initialNonce: nonce, - }, - writer: writer}, nil -} - -// Write encrypts and writes bytes. It encrypts when necessary and buffers extra -// plaintext bytes for next call. When the stream is finished, Close() MUST be -// called to append the final tag. -func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) { - // Append plaintextBytes to existing buffered bytes - n, err = aw.buffer.Write(plaintextBytes) - if err != nil { - return n, err - } - // Encrypt and write chunks - for aw.buffer.Len() >= aw.chunkSize { - plainChunk := aw.buffer.Next(aw.chunkSize) - encryptedChunk, err := aw.sealChunk(plainChunk) - if err != nil { - return n, err - } - _, err = aw.writer.Write(encryptedChunk) - if err != nil { - return n, err - } - } - return -} - -// Close encrypts and writes the remaining buffered plaintext if any, appends -// the final authentication tag, and closes the embedded writer. This function -// MUST be called at the end of a stream. -func (aw *aeadEncrypter) Close() (err error) { - // Encrypt and write a chunk if there's buffered data left, or if we haven't - // written any chunks yet. - if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 { - plainChunk := aw.buffer.Bytes() - lastEncryptedChunk, err := aw.sealChunk(plainChunk) - if err != nil { - return err - } - _, err = aw.writer.Write(lastEncryptedChunk) - if err != nil { - return err - } - } - // Compute final tag (associated data: packet tag, version, cipher, aead, - // chunk size, index, total number of encrypted octets). - adata := append(aw.associatedData[:], aw.chunkIndex[:]...) - adata = append(adata, make([]byte, 8)...) - binary.BigEndian.PutUint64(adata[13:], uint64(aw.bytesProcessed)) - nonce := aw.computeNextNonce() - finalTag := aw.aead.Seal(nil, nonce, nil, adata) - _, err = aw.writer.Write(finalTag) - if err != nil { - return err - } - return aw.writer.Close() -} - -// sealChunk Encrypts and authenticates the given chunk. -func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) { - if len(data) > aw.chunkSize { - return nil, errors.AEADError("chunk exceeds maximum length") - } - if aw.associatedData == nil { - return nil, errors.AEADError("can't seal without headers") - } - adata := append(aw.associatedData, aw.chunkIndex...) - nonce := aw.computeNextNonce() - encrypted := aw.aead.Seal(nil, nonce, data, adata) - aw.bytesProcessed += len(data) - if err := aw.aeadCrypter.incrementIndex(); err != nil { - return nil, err - } - return encrypted, nil -} - -// openChunk decrypts and checks integrity of an encrypted chunk, returning -// the underlying plaintext and an error. It access peeked bytes from next -// chunk, to identify the last chunk and decrypt/validate accordingly. -func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) { - tagLen := ar.aead.Overhead() - // Restore carried bytes from last call - chunkExtra := append(ar.peekedBytes, data...) - // 'chunk' contains encrypted bytes, followed by an authentication tag. - chunk := chunkExtra[:len(chunkExtra)-tagLen] - ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:] - adata := append(ar.associatedData, ar.chunkIndex...) - nonce := ar.computeNextNonce() - plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata) - if err != nil { - return nil, err - } - ar.bytesProcessed += len(plainChunk) - if err = ar.aeadCrypter.incrementIndex(); err != nil { - return nil, err - } - return plainChunk, nil -} - -// Checks the summary tag. It takes into account the total decrypted bytes into -// the associated data. It returns an error, or nil if the tag is valid. -func (ar *aeadDecrypter) validateFinalTag(tag []byte) error { - // Associated: tag, version, cipher, aead, chunk size, index, and octets - amountBytes := make([]byte, 8) - binary.BigEndian.PutUint64(amountBytes, uint64(ar.bytesProcessed)) - adata := append(ar.associatedData, ar.chunkIndex...) - adata = append(adata, amountBytes...) - nonce := ar.computeNextNonce() - _, err := ar.aead.Open(nil, nonce, tag, adata) - if err != nil { - return err - } - return nil -} - -// Associated data for chunks: tag, version, cipher, mode, chunk size byte +// associatedData for chunks: tag, version, cipher, mode, chunk size byte func (ae *AEADEncrypted) associatedData() []byte { return []byte{ 0xD4, @@ -332,33 +94,3 @@ func (ae *AEADEncrypted) associatedData() []byte { byte(ae.mode), ae.chunkSizeByte} } - -// computeNonce takes the incremental index and computes an eXclusive OR with -// the least significant 8 bytes of the receivers' initial nonce (see sec. -// 5.16.1 and 5.16.2). It returns the resulting nonce. -func (wo *aeadCrypter) computeNextNonce() (nonce []byte) { - nonce = make([]byte, len(wo.initialNonce)) - copy(nonce, wo.initialNonce) - offset := len(wo.initialNonce) - 8 - for i := 0; i < 8; i++ { - nonce[i+offset] ^= wo.chunkIndex[i] - } - return -} - -// incrementIndex performs an integer increment by 1 of the integer represented by the -// slice, modifying it accordingly. -func (wo *aeadCrypter) incrementIndex() error { - index := wo.chunkIndex - if len(index) == 0 { - return errors.AEADError("Index has length 0") - } - for i := len(index) - 1; i >= 0; i-- { - if index[i] < 255 { - index[i]++ - return nil - } - index[i] = 0 - } - return errors.AEADError("cannot further increment index") -} diff --git a/openpgp/packet/aead_encrypted_test.go b/openpgp/packet/aead_encrypted_test.go index 93cc5526..f5827579 100644 --- a/openpgp/packet/aead_encrypted_test.go +++ b/openpgp/packet/aead_encrypted_test.go @@ -9,6 +9,8 @@ import ( "io" mathrand "math/rand" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) // Note: This implementation does not produce packets with chunk sizes over @@ -16,10 +18,7 @@ import ( // them within limits of the running system. See RFC4880bis, sec 5.16. var maxChunkSizeExp = 62 -const ( - keyLength = 16 - maxPlaintextLength = 1 << 18 -) +const maxPlaintextLength = 1 << 18 func TestAeadRFCParse(t *testing.T) { for _, sample := range samplesAeadEncryptedDataPacket { @@ -274,9 +273,7 @@ func TestAeadUnclosedStreamRandomizeSlow(t *testing.T) { } // 'writeCloser' encrypts and writes the plaintext bytes. rawCipher := bytes.NewBuffer(nil) - writeCloser, err := SerializeAEADEncrypted( - rawCipher, key, config.Cipher(), config.AEAD().Mode(), config, - ) + writeCloser, err := SerializeAEADEncrypted(rawCipher, key, config) if err != nil { t.Error(err) } @@ -330,7 +327,6 @@ func randomConfig() *Config { var modes = []AEADMode{ AEADModeEAX, AEADModeOCB, - AEADModeExperimentalGCM, } // Random chunk size @@ -361,9 +357,7 @@ func randomStream(key []byte, ptLen int, config *Config) (*bytes.Buffer, []byte, // 'writeCloser' encrypts and writes the plaintext bytes. rawCipher := bytes.NewBuffer(nil) - writeCloser, err := SerializeAEADEncrypted( - rawCipher, key, config.Cipher(), config.AEAD().Mode(), config, - ) + writeCloser, err := SerializeAEADEncrypted(rawCipher, key, config) if err != nil { return nil, nil, err } @@ -399,3 +393,54 @@ func readDecryptedStream(rc io.ReadCloser) (got []byte, err error) { } return got, err } + +// SerializeAEADEncrypted initializes the aeadCrypter and returns a writer. +// This writer encrypts and writes bytes (see aeadEncrypter.Write()). +// This funcion is moved to the test suite to prevent it from creating this deprecated package +func SerializeAEADEncrypted(w io.Writer, key []byte, config *Config) (io.WriteCloser, error) { + writeCloser := noOpCloser{w} + writer, err := serializeStreamHeader(writeCloser, packetTypeAEADEncrypted) + if err != nil { + return nil, err + } + + // Data for en/decryption: tag, version, cipher, aead mode, chunk size + aeadConf := config.AEAD() + prefix := []byte{ + 0xD4, + aeadEncryptedVersion, + byte(config.Cipher()), + byte(aeadConf.Mode()), + aeadConf.ChunkSizeByte(), + } + n, err := writer.Write(prefix[1:]) + if err != nil || n < 4 { + return nil, errors.AEADError("could not write AEAD headers") + } + // Sample nonce + nonceLen := aeadConf.Mode().IvLength() + nonce := make([]byte, nonceLen) + n, err = rand.Read(nonce) + if err != nil { + panic("Could not sample random nonce") + } + _, err = writer.Write(nonce) + if err != nil { + return nil, err + } + blockCipher := CipherFunction(config.Cipher()).new(key) + alg := aeadConf.Mode().new(blockCipher) + + chunkSize := decodeAEADChunkSize(aeadConf.ChunkSizeByte()) + return &aeadEncrypter{ + aeadCrypter: aeadCrypter{ + aead: alg, + chunkSize: chunkSize, + associatedData: prefix, + chunkIndex: make([]byte, 8), + initialNonce: nonce, + packetTag: packetTypeAEADEncrypted, + }, + writer: writer, + }, nil +} diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index f9208158..04994bec 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -10,6 +10,8 @@ import ( "io" "math/big" "time" + + "github.com/ProtonMail/go-crypto/openpgp/s2k" ) // Config collects a number of parameters along with sensible defaults. @@ -33,16 +35,24 @@ type Config struct { DefaultCompressionAlgo CompressionAlgo // CompressionConfig configures the compression settings. CompressionConfig *CompressionConfig - // S2KCount is only used for symmetric encryption. It - // determines the strength of the passphrase stretching when + // S2K (String to Key) config, used for key derivation in the context of secret key encryption + // and password-encrypted data. + // If nil, the default configuration is used + S2KConfig *s2k.Config + // Iteration count for Iterated S2K (String to Key). + // Only used if sk2.Mode is nil. + // This value is duplicated here from s2k.Config for backwards compatibility. + // It determines the strength of the passphrase stretching when // the said passphrase is hashed to produce a key. S2KCount - // should be between 1024 and 65011712, inclusive. If Config - // is nil or S2KCount is 0, the value 65536 used. Not all + // should be between 65536 and 65011712, inclusive. If Config + // is nil or S2KCount is 0, the value 16777216 used. Not all // values in the above range can be represented. S2KCount will // be rounded up to the next representable value if it cannot // be encoded exactly. When set, it is strongly encrouraged to // use a value that is at least 65536. See RFC 4880 Section // 3.7.1.3. + // + // Deprecated: SK2Count should be configured in S2KConfig instead. S2KCount int // RSABits is the number of bits in new RSA keys made with NewEntity. // If zero, then 2048 bit keys are created. @@ -94,6 +104,12 @@ type Config struct { // 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. InsecureAllowUnauthenticatedMessages bool + // 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 + // signature subpackets. + KnownNotations map[string]bool + // SignatureNotations is a list of Notations to be added to any signatures. + SignatureNotations []*Notation } func (c *Config) Random() io.Reader { @@ -119,9 +135,9 @@ func (c *Config) Cipher() CipherFunction { func (c *Config) Now() time.Time { if c == nil || c.Time == nil { - return time.Now() + return time.Now().Truncate(time.Second) } - return c.Time() + return c.Time().Truncate(time.Second) } // KeyLifetime returns the validity period of the key. @@ -147,13 +163,6 @@ func (c *Config) Compression() CompressionAlgo { return c.DefaultCompressionAlgo } -func (c *Config) PasswordHashIterations() int { - if c == nil || c.S2KCount == 0 { - return 0 - } - return c.S2KCount -} - func (c *Config) RSAModulusBits() int { if c == nil || c.RSABits == 0 { return 2048 @@ -175,6 +184,27 @@ func (c *Config) CurveName() Curve { return c.Curve } +// Deprecated: The hash iterations should now be queried via the S2K() method. +func (c *Config) PasswordHashIterations() int { + if c == nil || c.S2KCount == 0 { + return 0 + } + return c.S2KCount +} + +func (c *Config) S2K() *s2k.Config { + if c == nil { + return nil + } + // for backwards compatibility + if c != nil && c.S2KCount > 0 && c.S2KConfig == nil { + return &s2k.Config{ + S2KCount: c.S2KCount, + } + } + return c.S2KConfig +} + func (c *Config) AEAD() *AEADConfig { if c == nil { return nil @@ -202,3 +232,17 @@ func (c *Config) AllowUnauthenticatedMessages() bool { } return c.InsecureAllowUnauthenticatedMessages } + +func (c *Config) KnownNotation(notationName string) bool { + if c == nil { + return false + } + return c.KnownNotations[notationName] +} + +func (c *Config) Notations() []*Notation { + if c == nil { + return nil + } + return c.SignatureNotations +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 801aec92..eeff2902 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -25,7 +25,7 @@ const encryptedKeyVersion = 3 type EncryptedKey struct { KeyId uint64 Algo PublicKeyAlgorithm - CipherFunc CipherFunction // only valid after a successful Decrypt + CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt encryptedMPI1, encryptedMPI2 encoding.Field @@ -123,6 +123,10 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { } e.CipherFunc = CipherFunction(b[0]) + if !e.CipherFunc.IsSupported() { + return errors.UnsupportedError("unsupported encryption function") + } + e.Key = b[1 : len(b)-2] expectedChecksum := uint16(b[len(b)-2])<<8 | uint16(b[len(b)-1]) checksum := checksumKeyMaterial(e.Key) diff --git a/openpgp/packet/fuzz_test.go b/openpgp/packet/fuzz_test.go new file mode 100644 index 00000000..2652365b --- /dev/null +++ b/openpgp/packet/fuzz_test.go @@ -0,0 +1,16 @@ +//go:build go1.18 +// +build go1.18 + +package packet + +import ( + "bytes" + "testing" +) + +func FuzzPackets(f *testing.F) { + f.Add([]byte("\x980\x040000\x16\t+\x06\x01\x04\x01\xdaG\x0f\x01\x00\x00")) + f.Fuzz(func(t *testing.T, data []byte) { + _, _ = Read(bytes.NewReader(data)) + }) +} diff --git a/openpgp/packet/notation.go b/openpgp/packet/notation.go new file mode 100644 index 00000000..2c3e3f50 --- /dev/null +++ b/openpgp/packet/notation.go @@ -0,0 +1,29 @@ +package packet + +// Notation type represents a Notation Data subpacket +// see https://tools.ietf.org/html/rfc4880#section-5.2.3.16 +type Notation struct { + Name string + Value []byte + IsCritical bool + IsHumanReadable bool +} + +func (notation *Notation) getData() []byte { + nameData := []byte(notation.Name) + nameLen := len(nameData) + valueLen := len(notation.Value) + + data := make([]byte, 8+nameLen+valueLen) + if notation.IsHumanReadable { + data[0] = 0x80 + } + + data[4] = byte(nameLen >> 8) + data[5] = byte(nameLen) + data[6] = byte(valueLen >> 8) + data[7] = byte(valueLen) + copy(data[8:8+nameLen], nameData) + copy(data[8+nameLen:], notation.Value) + return data +} diff --git a/openpgp/packet/notation_test.go b/openpgp/packet/notation_test.go new file mode 100644 index 00000000..9b12daf9 --- /dev/null +++ b/openpgp/packet/notation_test.go @@ -0,0 +1,38 @@ +package packet + +import ( + "bytes" + "testing" +) + +func TestNotationGetData(t *testing.T) { + notation := Notation{ + Name: "test@proton.me", + Value: []byte("test-value"), + IsCritical: true, + IsHumanReadable: true, + } + expected := []byte{0x80, 0, 0, 0, 0, 14, 0, 10} + expected = append(expected, []byte(notation.Name)...) + expected = append(expected, []byte(notation.Value)...) + data := notation.getData() + if !bytes.Equal(expected, data) { + t.Fatalf("Expected %s, got %s", expected, data) + } +} + +func TestNotationGetDataNotHumanReadable(t *testing.T) { + notation := Notation{ + Name: "test@proton.me", + Value: []byte("test-value"), + IsCritical: true, + IsHumanReadable: false, + } + expected := []byte{0, 0, 0, 0, 0, 14, 0, 10} + expected = append(expected, []byte(notation.Name)...) + expected = append(expected, []byte(notation.Value)...) + data := notation.getData() + if !bytes.Equal(expected, data) { + t.Fatalf("Expected %s, got %s", expected, data) + } +} diff --git a/openpgp/packet/one_pass_signature.go b/openpgp/packet/one_pass_signature.go index 41c35de2..033fb2d7 100644 --- a/openpgp/packet/one_pass_signature.go +++ b/openpgp/packet/one_pass_signature.go @@ -8,7 +8,7 @@ import ( "crypto" "encoding/binary" "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "io" "strconv" ) @@ -37,7 +37,7 @@ func (ops *OnePassSignature) parse(r io.Reader) (err error) { } var ok bool - ops.Hash, ok = s2k.HashIdToHash(buf[2]) + ops.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2]) if !ok { return errors.UnsupportedError("hash function: " + strconv.Itoa(int(buf[2]))) } @@ -55,7 +55,7 @@ func (ops *OnePassSignature) Serialize(w io.Writer) error { buf[0] = onePassSignatureVersion buf[1] = uint8(ops.SigType) var ok bool - buf[2], ok = s2k.HashToHashId(ops.Hash) + buf[2], ok = algorithm.HashToHashIdWithSha1(ops.Hash) if !ok { return errors.UnsupportedError("hash type: " + strconv.Itoa(int(ops.Hash))) } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index e930ff88..4d86a7da 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -302,21 +302,21 @@ func consumeAll(r io.Reader) (n int64, err error) { type packetType uint8 const ( - packetTypeEncryptedKey packetType = 1 - packetTypeSignature packetType = 2 - packetTypeSymmetricKeyEncrypted packetType = 3 - packetTypeOnePassSignature packetType = 4 - packetTypePrivateKey packetType = 5 - packetTypePublicKey packetType = 6 - packetTypePrivateSubkey packetType = 7 - packetTypeCompressed packetType = 8 - packetTypeSymmetricallyEncrypted packetType = 9 - packetTypeLiteralData packetType = 11 - packetTypeUserId packetType = 13 - packetTypePublicSubkey packetType = 14 - packetTypeUserAttribute packetType = 17 - packetTypeSymmetricallyEncryptedMDC packetType = 18 - packetTypeAEADEncrypted packetType = 20 + packetTypeEncryptedKey packetType = 1 + packetTypeSignature packetType = 2 + packetTypeSymmetricKeyEncrypted packetType = 3 + packetTypeOnePassSignature packetType = 4 + packetTypePrivateKey packetType = 5 + packetTypePublicKey packetType = 6 + packetTypePrivateSubkey packetType = 7 + packetTypeCompressed packetType = 8 + packetTypeSymmetricallyEncrypted packetType = 9 + packetTypeLiteralData packetType = 11 + packetTypeUserId packetType = 13 + packetTypePublicSubkey packetType = 14 + packetTypeUserAttribute packetType = 17 + packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18 + packetTypeAEADEncrypted packetType = 20 ) // EncryptedDataPacket holds encrypted data. It is currently implemented by @@ -361,9 +361,9 @@ func Read(r io.Reader) (p Packet, err error) { p = new(UserId) case packetTypeUserAttribute: p = new(UserAttribute) - case packetTypeSymmetricallyEncryptedMDC: + case packetTypeSymmetricallyEncryptedIntegrityProtected: se := new(SymmetricallyEncrypted) - se.MDC = true + se.IntegrityProtected = true p = se case packetTypeAEADEncrypted: p = new(AEADEncrypted) @@ -384,18 +384,18 @@ func Read(r io.Reader) (p Packet, err error) { type SignatureType uint8 const ( - SigTypeBinary SignatureType = 0x00 - SigTypeText = 0x01 - SigTypeGenericCert = 0x10 - SigTypePersonaCert = 0x11 - SigTypeCasualCert = 0x12 - SigTypePositiveCert = 0x13 - SigTypeSubkeyBinding = 0x18 - SigTypePrimaryKeyBinding = 0x19 - SigTypeDirectSignature = 0x1F - SigTypeKeyRevocation = 0x20 - SigTypeSubkeyRevocation = 0x28 - SigTypeCertificationRevocation = 0x30 + SigTypeBinary SignatureType = 0x00 + SigTypeText = 0x01 + SigTypeGenericCert = 0x10 + SigTypePersonaCert = 0x11 + SigTypeCasualCert = 0x12 + SigTypePositiveCert = 0x13 + SigTypeSubkeyBinding = 0x18 + SigTypePrimaryKeyBinding = 0x19 + SigTypeDirectSignature = 0x1F + SigTypeKeyRevocation = 0x20 + SigTypeSubkeyRevocation = 0x28 + SigTypeCertificationRevocation = 0x30 ) // PublicKeyAlgorithm represents the different public key system specified for @@ -455,6 +455,11 @@ func (cipher CipherFunction) KeySize() int { return algorithm.CipherFunction(cipher).KeySize() } +// IsSupported returns true if the cipher is supported from the library +func (cipher CipherFunction) IsSupported() bool { + return algorithm.CipherFunction(cipher).KeySize() > 0 +} + // blockSize returns the block size, in bytes, of cipher. func (cipher CipherFunction) blockSize() int { return algorithm.CipherFunction(cipher).BlockSize() @@ -490,15 +495,16 @@ const ( // AEADMode represents the different Authenticated Encryption with Associated // Data specified for OpenPGP. +// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 type AEADMode algorithm.AEADMode const ( - AEADModeEAX AEADMode = 1 - AEADModeOCB AEADMode = 2 - AEADModeExperimentalGCM AEADMode = 100 + AEADModeEAX AEADMode = 1 + AEADModeOCB AEADMode = 2 + AEADModeGCM AEADMode = 3 ) -func (mode AEADMode) NonceLength() int { +func (mode AEADMode) IvLength() int { return algorithm.AEADMode(mode).NonceLength() } @@ -527,19 +533,19 @@ const ( type Curve string const ( - Curve25519 Curve = "Curve25519" - Curve448 Curve = "Curve448" - CurveNistP256 Curve = "P256" - CurveNistP384 Curve = "P384" - CurveNistP521 Curve = "P521" - CurveSecP256k1 Curve = "SecP256k1" + Curve25519 Curve = "Curve25519" + Curve448 Curve = "Curve448" + CurveNistP256 Curve = "P256" + CurveNistP384 Curve = "P384" + CurveNistP521 Curve = "P521" + CurveSecP256k1 Curve = "SecP256k1" CurveBrainpoolP256 Curve = "BrainpoolP256" CurveBrainpoolP384 Curve = "BrainpoolP384" CurveBrainpoolP512 Curve = "BrainpoolP512" ) // TrustLevel represents a trust level per RFC4880 5.2.3.13 -type TrustLevel uint8 +type TrustLevel uint8 // TrustAmount represents a trust amount per RFC4880 5.2.3.13 type TrustAmount uint8 diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 009f0ef1..2fc43864 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -49,7 +49,7 @@ type PrivateKey struct { s2kParams *s2k.Params } -//S2KType s2k packet type +// S2KType s2k packet type type S2KType uint8 const ( @@ -179,6 +179,9 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { return } pk.cipher = CipherFunction(buf[0]) + if pk.cipher != 0 && !pk.cipher.IsSupported() { + return errors.UnsupportedError("unsupported cipher function in private key") + } pk.s2kParams, err = s2k.ParseIntoParams(r) if err != nil { return @@ -367,8 +370,8 @@ func serializeECDHPrivateKey(w io.Writer, priv *ecdh.PrivateKey) error { return err } -// Decrypt decrypts an encrypted private key using a passphrase. -func (pk *PrivateKey) Decrypt(passphrase []byte) error { +// decrypt decrypts an encrypted private key using a decryption key. +func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { return errors.ErrDummyPrivateKey("dummy key found") } @@ -376,9 +379,7 @@ func (pk *PrivateKey) Decrypt(passphrase []byte) error { return nil } - key := make([]byte, pk.cipher.KeySize()) - pk.s2k(key, passphrase) - block := pk.cipher.new(key) + block := pk.cipher.new(decryptionKey) cfb := cipher.NewCFBDecrypter(block, pk.iv) data := make([]byte, len(pk.encryptedData)) @@ -427,35 +428,79 @@ func (pk *PrivateKey) Decrypt(passphrase []byte) error { return nil } -// Encrypt encrypts an unencrypted private key using a passphrase. -func (pk *PrivateKey) Encrypt(passphrase []byte) error { - priv := bytes.NewBuffer(nil) - err := pk.serializePrivateKey(priv) +func (pk *PrivateKey) decryptWithCache(passphrase []byte, keyCache *s2k.Cache) error { + if pk.Dummy() { + return errors.ErrDummyPrivateKey("dummy key found") + } + if !pk.Encrypted { + return nil + } + + key, err := keyCache.GetOrComputeDerivedKey(passphrase, pk.s2kParams, pk.cipher.KeySize()) if err != nil { return err } + return pk.decrypt(key) +} + +// Decrypt decrypts an encrypted private key using a passphrase. +func (pk *PrivateKey) Decrypt(passphrase []byte) error { + if pk.Dummy() { + return errors.ErrDummyPrivateKey("dummy key found") + } + if !pk.Encrypted { + return nil + } + + key := make([]byte, pk.cipher.KeySize()) + pk.s2k(key, passphrase) + return pk.decrypt(key) +} - //Default config of private key encryption - pk.cipher = CipherAES256 - s2kConfig := &s2k.Config{ - S2KMode: 3, //Iterated - S2KCount: 65536, - Hash: crypto.SHA256, +// DecryptPrivateKeys decrypts all encrypted keys with the given config and passphrase. +// Avoids recomputation of similar s2k key derivations. +func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error { + // Create a cache to avoid recomputation of key derviations for the same passphrase. + s2kCache := &s2k.Cache{} + for _, key := range keys { + if key != nil && !key.Dummy() && key.Encrypted { + err := key.decryptWithCache(passphrase, s2kCache) + if err != nil { + return err + } + } } + return nil +} - pk.s2kParams, err = s2k.Generate(rand.Reader, s2kConfig) +// encrypt encrypts an unencrypted private key. +func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction CipherFunction) error { + if pk.Dummy() { + return errors.ErrDummyPrivateKey("dummy key found") + } + if pk.Encrypted { + return nil + } + // check if encryptionKey has the correct size + if len(key) != cipherFunction.KeySize() { + return errors.InvalidArgumentError("supplied encryption key has the wrong size") + } + + priv := bytes.NewBuffer(nil) + err := pk.serializePrivateKey(priv) if err != nil { return err } - privateKeyBytes := priv.Bytes() - key := make([]byte, pk.cipher.KeySize()) - pk.sha1Checksum = true + pk.cipher = cipherFunction + pk.s2kParams = params pk.s2k, err = pk.s2kParams.Function() if err != nil { return err - } - pk.s2k(key, passphrase) + } + + privateKeyBytes := priv.Bytes() + pk.sha1Checksum = true block := pk.cipher.new(key) pk.iv = make([]byte, pk.cipher.blockSize()) _, err = rand.Read(pk.iv) @@ -486,6 +531,62 @@ func (pk *PrivateKey) Encrypt(passphrase []byte) error { return err } +// EncryptWithConfig encrypts an unencrypted private key using the passphrase and the config. +func (pk *PrivateKey) EncryptWithConfig(passphrase []byte, config *Config) error { + params, err := s2k.Generate(config.Random(), config.S2K()) + if err != nil { + return err + } + // Derive an encryption key with the configured s2k function. + key := make([]byte, config.Cipher().KeySize()) + s2k, err := params.Function() + if err != nil { + return err + } + s2k(key, passphrase) + // Encrypt the private key with the derived encryption key. + return pk.encrypt(key, params, config.Cipher()) +} + +// EncryptPrivateKeys encrypts all unencrypted keys with the given config and passphrase. +// Only derives one key from the passphrase, which is then used to encrypt each key. +func EncryptPrivateKeys(keys []*PrivateKey, passphrase []byte, config *Config) error { + params, err := s2k.Generate(config.Random(), config.S2K()) + if err != nil { + return err + } + // Derive an encryption key with the configured s2k function. + encryptionKey := make([]byte, config.Cipher().KeySize()) + s2k, err := params.Function() + if err != nil { + return err + } + s2k(encryptionKey, passphrase) + for _, key := range keys { + if key != nil && !key.Dummy() && !key.Encrypted { + err = key.encrypt(encryptionKey, params, config.Cipher()) + if err != nil { + return err + } + } + } + return nil +} + +// Encrypt encrypts an unencrypted private key using a passphrase. +func (pk *PrivateKey) Encrypt(passphrase []byte) error { + // Default config of private key encryption + config := &Config{ + S2KConfig: &s2k.Config{ + S2KMode: s2k.IteratedSaltedS2K, + S2KCount: 65536, + Hash: crypto.SHA256, + } , + DefaultCipher: CipherAES256, + } + return pk.EncryptWithConfig(passphrase, config) +} + func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { switch priv := pk.PrivateKey.(type) { case *rsa.PrivateKey: diff --git a/openpgp/packet/private_key_test.go b/openpgp/packet/private_key_test.go index 37b3b3dc..154766f9 100644 --- a/openpgp/packet/private_key_test.go +++ b/openpgp/packet/private_key_test.go @@ -23,6 +23,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/s2k" ) const maxMessageLength = 1 << 10 @@ -137,6 +138,71 @@ func TestExternalPrivateKeyEncryptDecryptRandomizeSlow(t *testing.T) { } } +func TestExternalPrivateKeyEncryptDecryptArgon2(t *testing.T) { + config := &Config{ + S2KConfig: &s2k.Config{S2KMode: s2k.Argon2S2K}, + } + for i, test := range privateKeyTests { + packet, err := Read(readerFromHex(test.privateKeyHex)) + if err != nil { + t.Errorf("#%d: failed to parse: %s", i, err) + continue + } + + privKey := packet.(*PrivateKey) + + if !privKey.Encrypted { + t.Errorf("#%d: private key isn't encrypted", i) + continue + } + + // Decrypt with the correct password + err = privKey.Decrypt([]byte("testing")) + if err != nil { + t.Errorf("#%d: failed to decrypt: %s", i, err) + continue + } + + // Encrypt with another (possibly empty) password + randomPassword := make([]byte, mathrand.Intn(30)) + rand.Read(randomPassword) + err = privKey.EncryptWithConfig(randomPassword, config) + if err != nil { + t.Errorf("#%d: failed to encrypt: %s", i, err) + continue + } + + // Try to decrypt with incorrect password + incorrect := make([]byte, 1+mathrand.Intn(30)) + for rand.Read(incorrect); bytes.Equal(incorrect, randomPassword); { + rand.Read(incorrect) + } + err = privKey.Decrypt(incorrect) + if err == nil { + t.Errorf("#%d: decrypted with incorrect password\nPassword is:%vDecrypted with:%v", i, randomPassword, incorrect) + continue + } + + // Try to decrypt with old password + err = privKey.Decrypt([]byte("testing")) + if err == nil { + t.Errorf("#%d: decrypted with old password", i) + continue + } + + // Decrypt with correct password + err = privKey.Decrypt(randomPassword) + if err != nil { + t.Errorf("#%d: failed to decrypt: %s", i, err) + continue + } + + if !privKey.CreationTime.Equal(test.creationTime) || privKey.Encrypted { + t.Errorf("#%d: bad result, got: %#v", i, privKey) + } + } +} + func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) { h := hashFunc.New() if _, err := h.Write(msg); err != nil { diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 212ea43a..eca639d9 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -415,6 +415,10 @@ func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) { return } + if len(pk.p.Bytes()) == 0 { + return errors.StructuralError("empty EdDSA public key") + } + pub := eddsa.NewPublicKey(c) switch flag := pk.p.Bytes()[0]; flag { @@ -596,7 +600,7 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro } signed.Write(sig.HashSuffix) hashBytes := signed.Sum(nil) - if hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1] { + if sig.Version == 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) { return errors.SignatureError("hash tag doesn't match") } diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index c4914254..2cc8569f 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -17,8 +17,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" - "github.com/ProtonMail/go-crypto/openpgp/s2k" ) const ( @@ -66,21 +66,22 @@ type Signature struct { SigLifetimeSecs, KeyLifetimeSecs *uint32 PreferredSymmetric, PreferredHash, PreferredCompression []uint8 - PreferredAEAD []uint8 + PreferredCipherSuites [][2]uint8 IssuerKeyId *uint64 IssuerFingerprint []byte SignerUserId *string IsPrimaryId *bool + Notations []*Notation - // TrustLevel and TrustAmount can be set by the signer to assert that - // the key is not only valid but also trustworthy at the specified - // level. - // See RFC 4880, section 5.2.3.13 for details. - TrustLevel TrustLevel + // TrustLevel and TrustAmount can be set by the signer to assert that + // the key is not only valid but also trustworthy at the specified + // level. + // See RFC 4880, section 5.2.3.13 for details. + TrustLevel TrustLevel TrustAmount TrustAmount // TrustRegularExpression can be used in conjunction with trust Signature - // packets to limit the scope of the trust that is extended. + // packets to limit the scope of the trust that is extended. // See RFC 4880, section 5.2.3.14 for details. TrustRegularExpression *string @@ -101,8 +102,8 @@ type Signature struct { // In a self-signature, these flags are set there is a features subpacket // indicating that the issuer implementation supports these features - // (section 5.2.5.25). - MDC, AEAD, V5Keys bool + // see https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#features-subpacket + SEIPDv1, SEIPDv2 bool // EmbeddedSignature, if non-nil, is a signature of the parent key, by // this key. This prevents an attacker from claiming another's signing @@ -138,7 +139,13 @@ func (sig *Signature) parse(r io.Reader) (err error) { } var ok bool - sig.Hash, ok = s2k.HashIdToHash(buf[2]) + + if sig.Version < 5 { + sig.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2]) + } else { + sig.Hash, ok = algorithm.HashIdToHash(buf[2]) + } + if !ok { return errors.UnsupportedError("hash function " + strconv.Itoa(int(buf[2]))) } @@ -149,7 +156,11 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err != nil { return } - sig.buildHashSuffix(hashedSubpackets) + err = sig.buildHashSuffix(hashedSubpackets) + if err != nil { + return + } + err = parseSignatureSubpackets(sig, hashedSubpackets, true) if err != nil { return @@ -238,6 +249,7 @@ const ( keyExpirationSubpacket signatureSubpacketType = 9 prefSymmetricAlgosSubpacket signatureSubpacketType = 11 issuerSubpacket signatureSubpacketType = 16 + notationDataSubpacket signatureSubpacketType = 20 prefHashAlgosSubpacket signatureSubpacketType = 21 prefCompressionSubpacket signatureSubpacketType = 22 primaryUserIdSubpacket signatureSubpacketType = 25 @@ -248,7 +260,7 @@ const ( featuresSubpacket signatureSubpacketType = 30 embeddedSignatureSubpacket signatureSubpacketType = 32 issuerFingerprintSubpacket signatureSubpacketType = 33 - prefAeadAlgosSubpacket signatureSubpacketType = 34 + prefCipherSuitesSubpacket signatureSubpacketType = 39 ) // parseSignatureSubpacket parses a single subpacket. len(subpacket) is >= 1. @@ -259,6 +271,10 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r packetType signatureSubpacketType isCritical bool ) + if len(subpacket) == 0 { + err = errors.StructuralError("zero length signature subpacket") + return + } switch { case subpacket[0] < 192: length = uint32(subpacket[0]) @@ -292,12 +308,14 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r isCritical = subpacket[0]&0x80 == 0x80 subpacket = subpacket[1:] sig.rawSubpackets = append(sig.rawSubpackets, outputSubpacket{isHashed, packetType, isCritical, subpacket}) + if !isHashed && + packetType != issuerSubpacket && + packetType != issuerFingerprintSubpacket && + packetType != embeddedSignatureSubpacket { + return + } switch packetType { case creationTimeSubpacket: - if !isHashed { - err = errors.StructuralError("signature creation time in non-hashed area") - return - } if len(subpacket) != 4 { err = errors.StructuralError("signature creation time not four bytes") return @@ -306,9 +324,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r sig.CreationTime = time.Unix(int64(t), 0) case signatureExpirationSubpacket: // Signature expiration time, section 5.2.3.10 - if !isHashed { - return - } if len(subpacket) != 4 { err = errors.StructuralError("expiration subpacket with bad length") return @@ -316,10 +331,18 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r sig.SigLifetimeSecs = new(uint32) *sig.SigLifetimeSecs = binary.BigEndian.Uint32(subpacket) case trustSubpacket: + if len(subpacket) != 2 { + err = errors.StructuralError("trust subpacket with bad length") + return + } // Trust level and amount, section 5.2.3.13 sig.TrustLevel = TrustLevel(subpacket[0]) sig.TrustAmount = TrustAmount(subpacket[1]) case regularExpressionSubpacket: + if len(subpacket) == 0 { + err = errors.StructuralError("regexp subpacket with bad length") + return + } // Trust regular expression, section 5.2.3.14 // RFC specifies the string should be null-terminated; remove a null byte from the end if subpacket[len(subpacket)-1] != 0x00 { @@ -330,9 +353,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r sig.TrustRegularExpression = &trustRegularExpression case keyExpirationSubpacket: // Key expiration time, section 5.2.3.6 - if !isHashed { - return - } if len(subpacket) != 4 { err = errors.StructuralError("key expiration subpacket with bad length") return @@ -341,41 +361,52 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r *sig.KeyLifetimeSecs = binary.BigEndian.Uint32(subpacket) case prefSymmetricAlgosSubpacket: // Preferred symmetric algorithms, section 5.2.3.7 - if !isHashed { - return - } sig.PreferredSymmetric = make([]byte, len(subpacket)) copy(sig.PreferredSymmetric, subpacket) case issuerSubpacket: + // Issuer, section 5.2.3.5 if sig.Version > 4 { err = errors.StructuralError("issuer subpacket found in v5 key") + return } - // Issuer, section 5.2.3.5 if len(subpacket) != 8 { err = errors.StructuralError("issuer subpacket with bad length") return } sig.IssuerKeyId = new(uint64) *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket) - case prefHashAlgosSubpacket: - // Preferred hash algorithms, section 5.2.3.8 - if !isHashed { + case notationDataSubpacket: + // Notation data, section 5.2.3.16 + if len(subpacket) < 8 { + err = errors.StructuralError("notation data subpacket with bad length") return } + + nameLength := uint32(subpacket[4])<<8 | uint32(subpacket[5]) + valueLength := uint32(subpacket[6])<<8 | uint32(subpacket[7]) + if len(subpacket) != int(nameLength)+int(valueLength)+8 { + err = errors.StructuralError("notation data subpacket with bad length") + return + } + + notation := Notation{ + IsHumanReadable: (subpacket[0] & 0x80) == 0x80, + Name: string(subpacket[8:(nameLength + 8)]), + Value: subpacket[(nameLength + 8):(valueLength + nameLength + 8)], + IsCritical: isCritical, + } + + sig.Notations = append(sig.Notations, ¬ation) + case prefHashAlgosSubpacket: + // Preferred hash algorithms, section 5.2.3.8 sig.PreferredHash = make([]byte, len(subpacket)) copy(sig.PreferredHash, subpacket) case prefCompressionSubpacket: // Preferred compression algorithms, section 5.2.3.9 - if !isHashed { - return - } sig.PreferredCompression = make([]byte, len(subpacket)) copy(sig.PreferredCompression, subpacket) case primaryUserIdSubpacket: // Primary User ID, section 5.2.3.19 - if !isHashed { - return - } if len(subpacket) != 1 { err = errors.StructuralError("primary user id subpacket with bad length") return @@ -386,9 +417,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } case keyFlagsSubpacket: // Key flags, section 5.2.3.21 - if !isHashed { - return - } if len(subpacket) == 0 { err = errors.StructuralError("empty key flags subpacket") return @@ -420,9 +448,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r sig.SignerUserId = &userId case reasonForRevocationSubpacket: // Reason For Revocation, section 5.2.3.23 - if !isHashed { - return - } if len(subpacket) == 0 { err = errors.StructuralError("empty revocation reason subpacket") return @@ -434,18 +459,13 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r // Features subpacket, section 5.2.3.24 specifies a very general // mechanism for OpenPGP implementations to signal support for new // features. - if !isHashed { - return - } if len(subpacket) > 0 { if subpacket[0]&0x01 != 0 { - sig.MDC = true - } - if subpacket[0]&0x02 != 0 { - sig.AEAD = true + sig.SEIPDv1 = true } - if subpacket[0]&0x04 != 0 { - sig.V5Keys = true + // 0x02 and 0x04 are reserved + if subpacket[0]&0x08 != 0 { + sig.SEIPDv2 = true } } case embeddedSignatureSubpacket: @@ -468,11 +488,12 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } case policyUriSubpacket: // Policy URI, section 5.2.3.20 - if !isHashed { - return - } sig.PolicyURI = string(subpacket) case issuerFingerprintSubpacket: + if len(subpacket) == 0 { + err = errors.StructuralError("empty issuer fingerprint subpacket") + return + } v, l := subpacket[0], len(subpacket[1:]) if v == 5 && l != 32 || v != 5 && l != 20 { return nil, errors.StructuralError("bad fingerprint length") @@ -485,13 +506,19 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } else { *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket[13:21]) } - case prefAeadAlgosSubpacket: - // Preferred symmetric algorithms, section 5.2.3.8 - if !isHashed { + case prefCipherSuitesSubpacket: + // Preferred AEAD cipher suites + // See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#name-preferred-aead-ciphersuites + if len(subpacket)%2 != 0 { + err = errors.StructuralError("invalid aead cipher suite length") return } - sig.PreferredAEAD = make([]byte, len(subpacket)) - copy(sig.PreferredAEAD, subpacket) + + sig.PreferredCipherSuites = make([][2]byte, len(subpacket)/2) + + for i := 0; i < len(subpacket)/2; i++ { + sig.PreferredCipherSuites[i] = [2]uint8{subpacket[2*i], subpacket[2*i+1]} + } default: if isCritical { err = errors.UnsupportedError("unknown critical signature subpacket type " + strconv.Itoa(int(packetType))) @@ -589,7 +616,15 @@ func (sig *Signature) SigExpired(currentTime time.Time) bool { // buildHashSuffix constructs the HashSuffix member of sig in preparation for signing. func (sig *Signature) buildHashSuffix(hashedSubpackets []byte) (err error) { - hash, ok := s2k.HashToHashId(sig.Hash) + var hashId byte + var ok bool + + if sig.Version < 5 { + hashId, ok = algorithm.HashToHashIdWithSha1(sig.Hash) + } else { + hashId, ok = algorithm.HashToHashId(sig.Hash) + } + if !ok { sig.HashSuffix = nil return errors.InvalidArgumentError("hash cannot be represented in OpenPGP: " + strconv.Itoa(int(sig.Hash))) @@ -599,7 +634,7 @@ func (sig *Signature) buildHashSuffix(hashedSubpackets []byte) (err error) { uint8(sig.Version), uint8(sig.SigType), uint8(sig.PubKeyAlgo), - uint8(hash), + uint8(hashId), uint8(len(hashedSubpackets) >> 8), uint8(len(hashedSubpackets)), }) @@ -884,7 +919,7 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp if sig.IssuerKeyId != nil && sig.Version == 4 { keyId := make([]byte, 8) binary.BigEndian.PutUint64(keyId, *sig.IssuerKeyId) - subpackets = append(subpackets, outputSubpacket{true, issuerSubpacket, true, keyId}) + subpackets = append(subpackets, outputSubpacket{true, issuerSubpacket, false, keyId}) } if sig.IssuerFingerprint != nil { contents := append([]uint8{uint8(issuer.Version)}, sig.IssuerFingerprint...) @@ -927,17 +962,25 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp subpackets = append(subpackets, outputSubpacket{true, keyFlagsSubpacket, false, []byte{flags}}) } + for _, notation := range sig.Notations { + subpackets = append( + subpackets, + outputSubpacket{ + true, + notationDataSubpacket, + notation.IsCritical, + notation.getData(), + }) + } + // The following subpackets may only appear in self-signatures. var features = byte(0x00) - if sig.MDC { + if sig.SEIPDv1 { features |= 0x01 } - if sig.AEAD { - features |= 0x02 - } - if sig.V5Keys { - features |= 0x04 + if sig.SEIPDv2 { + features |= 0x08 } if features != 0x00 { @@ -979,8 +1022,13 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp subpackets = append(subpackets, outputSubpacket{true, policyUriSubpacket, false, []uint8(sig.PolicyURI)}) } - if len(sig.PreferredAEAD) > 0 { - subpackets = append(subpackets, outputSubpacket{true, prefAeadAlgosSubpacket, false, sig.PreferredAEAD}) + if len(sig.PreferredCipherSuites) > 0 { + serialized := make([]byte, len(sig.PreferredCipherSuites)*2) + for i, cipherSuite := range sig.PreferredCipherSuites { + serialized[2*i] = cipherSuite[0] + serialized[2*i+1] = cipherSuite[1] + } + subpackets = append(subpackets, outputSubpacket{true, prefCipherSuitesSubpacket, false, serialized}) } // Revocation reason appears only in revocation signatures and is serialized as per section 5.2.3.23. diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index c4569a56..66930730 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -8,10 +8,13 @@ import ( "bytes" "crypto" "encoding/hex" + "strings" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/armor" ) -func TestSignatureRead(t *testing.T) { +func TestSignatureReadAndReserialize(t *testing.T) { packet, err := Read(readerFromHex(signatureDataHex)) if err != nil { t.Error(err) @@ -21,6 +24,62 @@ func TestSignatureRead(t *testing.T) { if !ok || sig.SigType != SigTypeBinary || sig.PubKeyAlgo != PubKeyAlgoRSA || sig.Hash != crypto.SHA1 { t.Errorf("failed to parse, got: %#v", packet) } + + serializedSig := new(bytes.Buffer) + err = sig.Serialize(serializedSig) + if err != nil { + t.Fatalf("Unable to reserialize signature, got %s", err) + } + + hexSig := hex.EncodeToString(serializedSig.Bytes()) + if hexSig != signatureDataHex { + t.Fatalf("Wrong signature serialized: expected %s, got %s", signatureDataHex, hexSig) + } +} + +func TestOnePassSignatureReadAndReserialize(t *testing.T) { + packet, err := Read(readerFromHex(onePassSignatureDataHex)) + if err != nil { + t.Error(err) + return + } + sig, ok := packet.(*OnePassSignature) + if !ok || sig.SigType != SigTypeBinary || sig.PubKeyAlgo != PubKeyAlgoRSA || sig.Hash != crypto.SHA1 { + t.Errorf("failed to parse, got: %#v", packet) + } + + serializedSig := new(bytes.Buffer) + err = sig.Serialize(serializedSig) + if err != nil { + t.Fatalf("Unable to reserialize one-pass signature, got %s", err) + } + + hexSig := hex.EncodeToString(serializedSig.Bytes()) + if hexSig != onePassSignatureDataHex { + t.Fatalf("Wrong one-pass signature serialized: expected %s, got %s", onePassSignatureDataHex, hexSig) + } +} + +func TestSignatureEmptyFingerprint(t *testing.T) { + armoredSig := `-----BEGIN PGP SIGNATURE----- + +wpQEEAEIAAgFAohuCQABIQAATHUEAIiL44Hde8vbjvtHwx71Pr+gdxP1WoCifxaD +JKBccKkn82LY1qkfj50BvG0znrloMeQpfLZX1ybHiJwXG0P+cTQJ8m4GkwxlhBkT +BhLGOpf6bhM+HhXONIyoG9qp2ZVpgdOoC3zrsUuHvWKelBT8a3t6mCaTDmpvEMf1 +ltm2aQaG +=ZWr8 +-----END PGP SIGNATURE----- + ` + unarmored, err := armor.Decode(strings.NewReader(armoredSig)) + if err != nil { + t.Error(err) + return + } + _, err = Read(unarmored.Body) + if err == nil { + t.Errorf("Expected a parsing error") + return + } } func TestSignatureReserialize(t *testing.T) { @@ -265,6 +324,8 @@ func TestSignatureWithTrustAndRegex(t *testing.T) { } } +const onePassSignatureDataHex = `c40d03000201ab105c91af38fb1501` + const signatureDataHex = "c2c05c04000102000605024cb45112000a0910ab105c91af38fb158f8d07ff5596ea368c5efe015bed6e78348c0f033c931d5f2ce5db54ce7f2a7e4b4ad64db758d65a7a71773edeab7ba2a9e0908e6a94a1175edd86c1d843279f045b021a6971a72702fcbd650efc393c5474d5b59a15f96d2eaad4c4c426797e0dcca2803ef41c6ff234d403eec38f31d610c344c06f2401c262f0993b2e66cad8a81ebc4322c723e0d4ba09fe917e8777658307ad8329adacba821420741009dfe87f007759f0982275d028a392c6ed983a0d846f890b36148c7358bdb8a516007fac760261ecd06076813831a36d0459075d1befa245ae7f7fb103d92ca759e9498fe60ef8078a39a3beda510deea251ea9f0a7f0df6ef42060f20780360686f3e400e" const signatureWithTrustDataHex = "c2ad0410010800210502886e09001621040f0bfb42b3b08bece556fffcc181c053de849bf20385013c000035d803ff405c3c10211d680d3f5192e44d5acf7a25068a9938b5e5b1337735658ef8916e6878735ddfe15679c4868fcf46f02890104a5fb7caffa8e628a202deeda8376d58e586d60c1759e667fa49d87c7564c83b88f59db2631dc7e68535fd4a13b6096f91b05f7bb9989ddb36fc7e6e35dcc2f493468320cbe66e27895744eab2ae4b" diff --git a/openpgp/packet/symmetric_key_encrypted.go b/openpgp/packet/symmetric_key_encrypted.go index d5b6a87f..bac2b132 100644 --- a/openpgp/packet/symmetric_key_encrypted.go +++ b/openpgp/packet/symmetric_key_encrypted.go @@ -14,8 +14,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/s2k" ) -// This is the largest session key that we'll support. Since no 512-bit cipher -// has even been seriously used, this is comfortably large. +// This is the largest session key that we'll support. Since at most 256-bit cipher +// is supported in OpenPGP, this is large enough to contain also the auth tag. const maxSessionKeySizeInBytes = 64 // SymmetricKeyEncrypted represents a passphrase protected session key. See RFC @@ -25,13 +25,16 @@ type SymmetricKeyEncrypted struct { CipherFunc CipherFunction Mode AEADMode s2k func(out, in []byte) - aeadNonce []byte - encryptedKey []byte + iv []byte + encryptedKey []byte // Contains also the authentication tag for AEAD } +// parse parses an SymmetricKeyEncrypted packet as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#name-symmetric-key-encrypted-ses func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { - // RFC 4880, section 5.3. - var buf [2]byte + var buf [1]byte + + // Version if _, err := readFull(r, buf[:]); err != nil { return err } @@ -39,17 +42,22 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { if ske.Version != 4 && ske.Version != 5 { return errors.UnsupportedError("unknown SymmetricKeyEncrypted version") } - ske.CipherFunc = CipherFunction(buf[1]) - if ske.CipherFunc.KeySize() == 0 { - return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[1]))) + + // Cipher function + if _, err := readFull(r, buf[:]); err != nil { + return err + } + ske.CipherFunc = CipherFunction(buf[0]) + if !ske.CipherFunc.IsSupported() { + return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[0]))) } if ske.Version == 5 { - mode := make([]byte, 1) - if _, err := r.Read(mode); err != nil { + // AEAD mode + if _, err := readFull(r, buf[:]); err != nil { return errors.StructuralError("cannot read AEAD octet from packet") } - ske.Mode = AEADMode(mode[0]) + ske.Mode = AEADMode(buf[0]) } var err error @@ -61,13 +69,14 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { } if ske.Version == 5 { - // AEAD nonce - nonce := make([]byte, ske.Mode.NonceLength()) - _, err := readFull(r, nonce) - if err != nil && err != io.ErrUnexpectedEOF { - return err + // AEAD IV + iv := make([]byte, ske.Mode.IvLength()) + _, err := readFull(r, iv) + if err != nil { + return errors.StructuralError("cannot read AEAD IV") } - ske.aeadNonce = nonce + + ske.iv = iv } encryptedKey := make([]byte, maxSessionKeySizeInBytes) @@ -128,11 +137,10 @@ func (ske *SymmetricKeyEncrypted) decryptV4(key []byte) ([]byte, CipherFunction, } func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) { - blockCipher := CipherFunction(ske.CipherFunc).new(key) - aead := ske.Mode.new(blockCipher) - adata := []byte{0xc3, byte(5), byte(ske.CipherFunc), byte(ske.Mode)} - plaintextKey, err := aead.Open(nil, ske.aeadNonce, ske.encryptedKey, adata) + aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata) + + plaintextKey, err := aead.Open(nil, ske.iv, ske.encryptedKey, adata) if err != nil { return nil, err } @@ -142,17 +150,12 @@ func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) { // SerializeSymmetricKeyEncrypted serializes a symmetric key packet to w. // The packet contains a random session key, encrypted by a key derived from // the given passphrase. The session key is returned and must be passed to -// SerializeSymmetricallyEncrypted or SerializeAEADEncrypted, depending on -// whether config.AEADConfig != nil. +// SerializeSymmetricallyEncrypted. // If config is nil, sensible defaults will be used. func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Config) (key []byte, err error) { cipherFunc := config.Cipher() - keySize := cipherFunc.KeySize() - if keySize == 0 { - return nil, errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(cipherFunc))) - } - sessionKey := make([]byte, keySize) + sessionKey := make([]byte, cipherFunc.KeySize()) _, err = io.ReadFull(config.Random(), sessionKey) if err != nil { return @@ -169,9 +172,8 @@ func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Conf // SerializeSymmetricKeyEncryptedReuseKey serializes a symmetric key packet to w. // The packet contains the given session key, encrypted by a key derived from -// the given passphrase. The session key must be passed to -// SerializeSymmetricallyEncrypted or SerializeAEADEncrypted, depending on -// whether config.AEADConfig != nil. +// the given passphrase. The returned session key must be passed to +// SerializeSymmetricallyEncrypted. // If config is nil, sensible defaults will be used. func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, config *Config) (err error) { var version int @@ -181,16 +183,17 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass version = 4 } cipherFunc := config.Cipher() - keySize := cipherFunc.KeySize() - if keySize == 0 { - return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(cipherFunc))) + // cipherFunc must be AES + if !cipherFunc.IsSupported() || cipherFunc < CipherAES128 || cipherFunc > CipherAES256 { + return errors.UnsupportedError("unsupported cipher: " + strconv.Itoa(int(cipherFunc))) } + keySize := cipherFunc.KeySize() s2kBuf := new(bytes.Buffer) keyEncryptingKey := make([]byte, keySize) // s2k.Serialize salts and stretches the passphrase, and writes the // resulting key to keyEncryptingKey and the s2k descriptor to s2kBuf. - err = s2k.Serialize(s2kBuf, keyEncryptingKey, config.Random(), passphrase, &s2k.Config{Hash: config.Hash(), S2KCount: config.PasswordHashIterations()}) + err = s2k.Serialize(s2kBuf, keyEncryptingKey, config.Random(), passphrase, config.S2K()) if err != nil { return } @@ -201,20 +204,20 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass case 4: packetLength = 2 /* header */ + len(s2kBytes) + 1 /* cipher type */ + keySize case 5: - nonceLen := config.AEAD().Mode().NonceLength() + ivLen := config.AEAD().Mode().IvLength() tagLen := config.AEAD().Mode().TagLength() - packetLength = 3 + len(s2kBytes) + nonceLen + keySize + tagLen + packetLength = 3 + len(s2kBytes) + ivLen + keySize + tagLen } err = serializeHeader(w, packetTypeSymmetricKeyEncrypted, packetLength) if err != nil { return } - buf := make([]byte, 2) // Symmetric Key Encrypted Version - buf[0] = byte(version) + buf := []byte{byte(version)} + // Cipher function - buf[1] = byte(cipherFunc) + buf = append(buf, byte(cipherFunc)) if version == 5 { // AEAD mode @@ -241,19 +244,20 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass return } case 5: - blockCipher := cipherFunc.new(keyEncryptingKey) mode := config.AEAD().Mode() - aead := mode.new(blockCipher) - // Sample nonce using random reader - nonce := make([]byte, config.AEAD().Mode().NonceLength()) - _, err = io.ReadFull(config.Random(), nonce) + adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)} + aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata) + + // Sample iv using random reader + iv := make([]byte, config.AEAD().Mode().IvLength()) + _, err = io.ReadFull(config.Random(), iv) if err != nil { return } // Seal and write (encryptedData includes auth. tag) - adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)} - encryptedData := aead.Seal(nil, nonce, sessionKey, adata) - _, err = w.Write(nonce) + + encryptedData := aead.Seal(nil, iv, sessionKey, adata) + _, err = w.Write(iv) if err != nil { return } @@ -265,3 +269,8 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass return } + +func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte) (aead cipher.AEAD) { + blockCipher := c.new(inputKey) + return mode.new(blockCipher) +} diff --git a/openpgp/packet/symmetric_key_encrypted_data_test.go b/openpgp/packet/symmetric_key_encrypted_data_test.go index 59353308..119405cb 100644 --- a/openpgp/packet/symmetric_key_encrypted_data_test.go +++ b/openpgp/packet/symmetric_key_encrypted_data_test.go @@ -1,8 +1,7 @@ package packet -// These test vectors contain V4 or V4 symmetric key encrypted packets followed -// by an integrity protected packet (either a SymmetricallyEncrypted (with MDC) -// or AEADEncrypted packet. +// These test vectors contain V4 or V5 symmetric key encrypted packets followed +// by an integrity protected packet (SEIPD v1 or v2). type packetSequence struct { password string @@ -10,22 +9,18 @@ type packetSequence struct { contents string } -var keyAndIpePackets = []*packetSequence{symEncTest, aeadEaxRFC, aeadOcbRFC} +var keyAndIpePackets = []*packetSequence{aeadEaxRFC, aeadOcbRFC} +// https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-complete-aead-eax-encrypted- var aeadEaxRFC = &packetSequence{ password: "password", packets: "c33e0507010308cd5a9f70fbe0bc6590bc669e34e500dcaedc5b32aa2dab02359dee19d07c3446c4312a34ae1967a2fb7e928ea5b4fa8012bd456d1738c63c36d44a0107010eb732379f73c4928de25facfe6517ec105dc11a81dc0cb8a2f6f3d90016384a56fc821ae11ae8dbcb49862655dea88d06a81486801b0ff387bd2eab013de1259586906eab2476", contents: "cb1462000000000048656c6c6f2c20776f726c64210a", } +// https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-complete-aead-ocb-encrypted- var aeadOcbRFC = &packetSequence{ password: "password", packets: "c33d05070203089f0b7da3e5ea64779099e326e5400a90936cefb4e8eba08c6773716d1f2714540a38fcac529949dac529d3de31e15b4aeb729e330033dbedd4490107020e5ed2bc1e470abe8f1d644c7a6c8a567b0f7701196611a154ba9c2574cd056284a8ef68035c623d93cc708a43211bb6eaf2b27f7c18d571bcd83b20add3a08b73af15b9a098", contents: "cb1462000000000048656c6c6f2c20776f726c64210a", } - -var symEncTest = &packetSequence{ - password: "password", - packets: "c32e04090308f9f479ee0862ee8700a86d5cce4c166b5a7d664dcbe0f0eb2696a3e8a815fe8913251605ad79cc865f15d24301c3da8f5003383b9bd62c673589e2292d990902227311905ff4a7f694727578468e15d9f1aadb41572c4b2a789d7f93896661249200b64af9fbf6abf001f5498d036a", - contents: "cb1875076d73672e7478745cafc23e636f6e74656e74732e0d0a", -} diff --git a/openpgp/packet/symmetric_key_encrypted_test.go b/openpgp/packet/symmetric_key_encrypted_test.go index 28f3f4cd..8294adb3 100644 --- a/openpgp/packet/symmetric_key_encrypted_test.go +++ b/openpgp/packet/symmetric_key_encrypted_test.go @@ -12,6 +12,8 @@ import ( "io/ioutil" mathrand "math/rand" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/s2k" ) const maxPassLen = 64 @@ -65,104 +67,126 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) { } } -func TestRandomSerializeSymmetricKeyEncryptedV5RandomizeSlow(t *testing.T) { - var ciphers = []CipherFunction{ - CipherAES128, - CipherAES192, - CipherAES256, - } - var modes = []AEADMode{ - AEADModeEAX, - AEADModeOCB, - AEADModeExperimentalGCM, +func TestSerializeSymmetricKeyEncryptedV5RandomizeSlow(t *testing.T) { + ciphers := map[string]CipherFunction{ + "AES128": CipherAES128, + "AES192": CipherAES192, + "AES256": CipherAES256, } - var buf bytes.Buffer - passphrase := make([]byte, mathrand.Intn(maxPassLen)) - _, err := rand.Read(passphrase) - if err != nil { - panic(err) - } - aeadConf := AEADConfig{ - DefaultMode: modes[mathrand.Intn(len(modes))], - } - config := &Config{ - DefaultCipher: ciphers[mathrand.Intn(len(ciphers))], - AEADConfig: &aeadConf, - } - key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) - p, err := Read(&buf) - if err != nil { - t.Errorf("failed to reparse %s", err) - } - ske, ok := p.(*SymmetricKeyEncrypted) - if !ok { - t.Errorf("parsed a different packet type: %#v", p) + modes := map[string]AEADMode{ + "EAX": AEADModeEAX, + "OCB": AEADModeOCB, + "GCM": AEADModeGCM, } - parsedKey, _, err := ske.Decrypt(passphrase) - if err != nil { - t.Errorf("failed to decrypt reparsed SKE: %s", err) + modesS2K := map[string]s2k.Mode{ + "Salted": s2k.SaltedS2K, + "Iterated": s2k.IteratedSaltedS2K, + "Argon2": s2k.Argon2S2K, } - if !bytes.Equal(key, parsedKey) { - t.Errorf("keys don't match after Decrypt: %x (original) vs %x (parsed)", key, parsedKey) + + for cipherName, cipher := range ciphers { + t.Run(cipherName, func(t *testing.T) { + for modeName, mode := range modes { + t.Run(modeName, func(t *testing.T) { + for s2kName, s2ktype := range modesS2K { + t.Run(s2kName, func(t *testing.T) { + var buf bytes.Buffer + passphrase := randomKey(mathrand.Intn(maxPassLen)) + + config := &Config{ + DefaultCipher: cipher, + AEADConfig: &AEADConfig{DefaultMode: mode}, + S2KConfig: &s2k.Config{S2KMode: s2ktype, PassphraseIsHighEntropy: true}, + } + + key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) + p, err := Read(&buf) + if err != nil { + t.Errorf("failed to reparse %s", err) + } + ske, ok := p.(*SymmetricKeyEncrypted) + if !ok { + t.Errorf("parsed a different packet type: %#v", p) + } + + parsedKey, _, err := ske.Decrypt(passphrase) + if err != nil { + t.Errorf("failed to decrypt reparsed SKE: %s", err) + } + if !bytes.Equal(key, parsedKey) { + t.Errorf("keys don't match after Decrypt: %x (original) vs %x (parsed)", key, parsedKey) + } + }) + } + }) + } + }) } } func TestSerializeSymmetricKeyEncryptedCiphersV4(t *testing.T) { - tests := [...]struct { - cipherFunc CipherFunction - name string - }{ - {Cipher3DES, "Cipher3DES"}, - {CipherCAST5, "CipherCAST5"}, - {CipherAES128, "CipherAES128"}, - {CipherAES192, "CipherAES192"}, - {CipherAES256, "CipherAES256"}, + tests := map[string]CipherFunction{ + "AES128": CipherAES128, + "AES192": CipherAES192, + "AES256": CipherAES256, } - for _, test := range tests { - var buf bytes.Buffer - passphrase := make([]byte, mathrand.Intn(maxPassLen)) - if _, err := rand.Read(passphrase); err != nil { - panic(err) - } - config := &Config{ - DefaultCipher: test.cipherFunc, - } + testS2K := map[string]s2k.Mode{ + "Salted": s2k.SaltedS2K, + "Iterated": s2k.IteratedSaltedS2K, + "Argon2": s2k.Argon2S2K, + } - key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) - if err != nil { - t.Errorf("cipher(%s) failed to serialize: %s", test.name, err) - continue - } + for cipherName, cipher := range tests { + t.Run(cipherName, func(t *testing.T) { + for s2kName, s2ktype := range testS2K { + t.Run(s2kName, func(t *testing.T) { + var buf bytes.Buffer + passphrase := make([]byte, mathrand.Intn(maxPassLen)) + if _, err := rand.Read(passphrase); err != nil { + panic(err) + } + config := &Config{ + DefaultCipher: cipher, + S2KConfig: &s2k.Config{ + S2KMode: s2ktype, + PassphraseIsHighEntropy: true, + }, + } - p, err := Read(&buf) - if err != nil { - t.Errorf("cipher(%s) failed to reparse: %s", test.name, err) - continue - } + key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) + if err != nil { + t.Fatalf("failed to serialize: %s", err) + } - ske, ok := p.(*SymmetricKeyEncrypted) - if !ok { - t.Errorf("cipher(%s) parsed a different packet type: %#v", test.name, p) - continue - } + p, err := Read(&buf) + if err != nil { + t.Fatalf("failed to reparse: %s", err) + } - if ske.CipherFunc != config.DefaultCipher { - t.Errorf("cipher(%s) SKE cipher function is %d (expected %d)", test.name, ske.CipherFunc, config.DefaultCipher) - } - parsedKey, parsedCipherFunc, err := ske.Decrypt(passphrase) - if err != nil { - t.Errorf("cipher(%s) failed to decrypt reparsed SKE: %s", test.name, err) - continue - } - if !bytes.Equal(key, parsedKey) { - t.Errorf("cipher(%s) keys don't match after Decrypt: %x (original) vs %x (parsed)", test.name, key, parsedKey) - } - if parsedCipherFunc != test.cipherFunc { - t.Errorf("cipher(%s) cipher function doesn't match after Decrypt: %d (original) vs %d (parsed)", - test.name, test.cipherFunc, parsedCipherFunc) - } + ske, ok := p.(*SymmetricKeyEncrypted) + if !ok { + t.Fatalf("parsed a different packet type: %#v", p) + } + + if ske.CipherFunc != config.DefaultCipher { + t.Fatalf("SKE cipher function is %d (expected %d)", ske.CipherFunc, config.DefaultCipher) + } + parsedKey, parsedCipherFunc, err := ske.Decrypt(passphrase) + if err != nil { + t.Fatalf("failed to decrypt reparsed SKE: %s", err) + } + if !bytes.Equal(key, parsedKey) { + t.Fatalf("keys don't match after Decrypt: %x (original) vs %x (parsed)", key, parsedKey) + } + if parsedCipherFunc != cipher { + t.Fatalf("cipher function doesn't match after Decrypt: %d (original) vs %d (parsed)", + cipher, parsedCipherFunc) + } + }) + } + }) } } diff --git a/openpgp/packet/symmetrically_encrypted.go b/openpgp/packet/symmetrically_encrypted.go index 8b84de17..e9bbf032 100644 --- a/openpgp/packet/symmetrically_encrypted.go +++ b/openpgp/packet/symmetrically_encrypted.go @@ -5,36 +5,54 @@ package packet import ( - "crypto/cipher" - "crypto/sha1" - "crypto/subtle" - "hash" "io" - "strconv" "github.com/ProtonMail/go-crypto/openpgp/errors" ) +const aeadSaltSize = 32 + // SymmetricallyEncrypted represents a symmetrically encrypted byte string. The // encrypted Contents will consist of more OpenPGP packets. See RFC 4880, // sections 5.7 and 5.13. type SymmetricallyEncrypted struct { - MDC bool // true iff this is a type 18 packet and thus has an embedded MAC. - Contents io.Reader - prefix []byte + Version int + Contents io.Reader // contains tag for version 2 + IntegrityProtected bool // If true it is type 18 (with MDC or AEAD). False is packet type 9 + + // Specific to version 1 + prefix []byte + + // Specific to version 2 + Cipher CipherFunction + Mode AEADMode + ChunkSizeByte byte + Salt [aeadSaltSize]byte } -const symmetricallyEncryptedVersion = 1 +const ( + symmetricallyEncryptedVersionMdc = 1 + symmetricallyEncryptedVersionAead = 2 +) func (se *SymmetricallyEncrypted) parse(r io.Reader) error { - if se.MDC { + if se.IntegrityProtected { // See RFC 4880, section 5.13. var buf [1]byte _, err := readFull(r, buf[:]) if err != nil { return err } - if buf[0] != symmetricallyEncryptedVersion { + + switch buf[0] { + case symmetricallyEncryptedVersionMdc: + se.Version = symmetricallyEncryptedVersionMdc + case symmetricallyEncryptedVersionAead: + se.Version = symmetricallyEncryptedVersionAead + if err := se.parseAead(r); err != nil { + return err + } + default: return errors.UnsupportedError("unknown SymmetricallyEncrypted version") } } @@ -46,245 +64,27 @@ func (se *SymmetricallyEncrypted) parse(r io.Reader) error { // packet can be read. An incorrect key will only be detected after trying // to decrypt the entire data. func (se *SymmetricallyEncrypted) Decrypt(c CipherFunction, key []byte) (io.ReadCloser, error) { - keySize := c.KeySize() - if keySize == 0 { - return nil, errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(c))) - } - if len(key) != keySize { - return nil, errors.InvalidArgumentError("SymmetricallyEncrypted: incorrect key length") - } - - if se.prefix == nil { - se.prefix = make([]byte, c.blockSize()+2) - _, err := readFull(se.Contents, se.prefix) - if err != nil { - return nil, err - } - } else if len(se.prefix) != c.blockSize()+2 { - return nil, errors.InvalidArgumentError("can't try ciphers with different block lengths") + if se.Version == symmetricallyEncryptedVersionAead { + return se.decryptAead(key) } - ocfbResync := OCFBResync - if se.MDC { - // MDC packets use a different form of OCFB mode. - ocfbResync = OCFBNoResync - } - - s := NewOCFBDecrypter(c.new(key), se.prefix, ocfbResync) - - plaintext := cipher.StreamReader{S: s, R: se.Contents} - - if se.MDC { - // MDC packets have an embedded hash that we need to check. - h := sha1.New() - h.Write(se.prefix) - return &seMDCReader{in: plaintext, h: h}, nil - } - - // Otherwise, we just need to wrap plaintext so that it's a valid ReadCloser. - return seReader{plaintext}, nil -} - -// seReader wraps an io.Reader with a no-op Close method. -type seReader struct { - in io.Reader -} - -func (ser seReader) Read(buf []byte) (int, error) { - return ser.in.Read(buf) -} - -func (ser seReader) Close() error { - return nil -} - -const mdcTrailerSize = 1 /* tag byte */ + 1 /* length byte */ + sha1.Size - -// An seMDCReader wraps an io.Reader, maintains a running hash and keeps hold -// of the most recent 22 bytes (mdcTrailerSize). Upon EOF, those bytes form an -// MDC packet containing a hash of the previous Contents which is checked -// against the running hash. See RFC 4880, section 5.13. -type seMDCReader struct { - in io.Reader - h hash.Hash - trailer [mdcTrailerSize]byte - scratch [mdcTrailerSize]byte - trailerUsed int - error bool - eof bool -} - -func (ser *seMDCReader) Read(buf []byte) (n int, err error) { - if ser.error { - err = io.ErrUnexpectedEOF - return - } - if ser.eof { - err = io.EOF - return - } - - // If we haven't yet filled the trailer buffer then we must do that - // first. - for ser.trailerUsed < mdcTrailerSize { - n, err = ser.in.Read(ser.trailer[ser.trailerUsed:]) - ser.trailerUsed += n - if err == io.EOF { - if ser.trailerUsed != mdcTrailerSize { - n = 0 - err = io.ErrUnexpectedEOF - ser.error = true - return - } - ser.eof = true - n = 0 - return - } - - if err != nil { - n = 0 - return - } - } - - // If it's a short read then we read into a temporary buffer and shift - // the data into the caller's buffer. - if len(buf) <= mdcTrailerSize { - n, err = readFull(ser.in, ser.scratch[:len(buf)]) - copy(buf, ser.trailer[:n]) - ser.h.Write(buf[:n]) - copy(ser.trailer[:], ser.trailer[n:]) - copy(ser.trailer[mdcTrailerSize-n:], ser.scratch[:]) - if n < len(buf) { - ser.eof = true - err = io.EOF - } - return - } - - n, err = ser.in.Read(buf[mdcTrailerSize:]) - copy(buf, ser.trailer[:]) - ser.h.Write(buf[:n]) - copy(ser.trailer[:], buf[n:]) - - if err == io.EOF { - ser.eof = true - } - return -} - -// This is a new-format packet tag byte for a type 19 (MDC) packet. -const mdcPacketTagByte = byte(0x80) | 0x40 | 19 - -func (ser *seMDCReader) Close() error { - if ser.error { - return errors.ErrMDCMissing - } - - for !ser.eof { - // We haven't seen EOF so we need to read to the end - var buf [1024]byte - _, err := ser.Read(buf[:]) - if err == io.EOF { - break - } - if err != nil { - return errors.ErrMDCMissing - } - } - - ser.h.Write(ser.trailer[:2]) - - final := ser.h.Sum(nil) - if subtle.ConstantTimeCompare(final, ser.trailer[2:]) != 1 { - return errors.ErrMDCHashMismatch - } - // The hash already includes the MDC header, but we still check its value - // to confirm encryption correctness - if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size { - return errors.ErrMDCMissing - } - return nil -} - -// An seMDCWriter writes through to an io.WriteCloser while maintains a running -// hash of the data written. On close, it emits an MDC packet containing the -// running hash. -type seMDCWriter struct { - w io.WriteCloser - h hash.Hash -} - -func (w *seMDCWriter) Write(buf []byte) (n int, err error) { - w.h.Write(buf) - return w.w.Write(buf) -} - -func (w *seMDCWriter) Close() (err error) { - var buf [mdcTrailerSize]byte - - buf[0] = mdcPacketTagByte - buf[1] = sha1.Size - w.h.Write(buf[:2]) - digest := w.h.Sum(nil) - copy(buf[2:], digest) - - _, err = w.w.Write(buf[:]) - if err != nil { - return - } - return w.w.Close() -} - -// noOpCloser is like an ioutil.NopCloser, but for an io.Writer. -type noOpCloser struct { - w io.Writer -} - -func (c noOpCloser) Write(data []byte) (n int, err error) { - return c.w.Write(data) -} - -func (c noOpCloser) Close() error { - return nil + return se.decryptMdc(c, key) } // SerializeSymmetricallyEncrypted serializes a symmetrically encrypted packet // to w and returns a WriteCloser to which the to-be-encrypted packets can be // written. // If config is nil, sensible defaults will be used. -func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, key []byte, config *Config) (Contents io.WriteCloser, err error) { - if c.KeySize() != len(key) { - return nil, errors.InvalidArgumentError("SymmetricallyEncrypted.Serialize: bad key length") - } +func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, aeadSupported bool, cipherSuite CipherSuite, key []byte, config *Config) (Contents io.WriteCloser, err error) { writeCloser := noOpCloser{w} - ciphertext, err := serializeStreamHeader(writeCloser, packetTypeSymmetricallyEncryptedMDC) - if err != nil { - return - } - - _, err = ciphertext.Write([]byte{symmetricallyEncryptedVersion}) + ciphertext, err := serializeStreamHeader(writeCloser, packetTypeSymmetricallyEncryptedIntegrityProtected) if err != nil { return } - block := c.new(key) - blockSize := block.BlockSize() - iv := make([]byte, blockSize) - _, err = config.Random().Read(iv) - if err != nil { - return - } - s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync) - _, err = ciphertext.Write(prefix) - if err != nil { - return + if aeadSupported { + return serializeSymmetricallyEncryptedAead(ciphertext, cipherSuite, config.AEADConfig.ChunkSizeByte(), config.Random(), key) } - plaintext := cipher.StreamWriter{S: s, W: ciphertext} - h := sha1.New() - h.Write(iv) - h.Write(iv[blockSize-2:]) - Contents = &seMDCWriter{w: plaintext, h: h} - return + return serializeSymmetricallyEncryptedMdc(ciphertext, c, key, config) } diff --git a/openpgp/packet/symmetrically_encrypted_aead.go b/openpgp/packet/symmetrically_encrypted_aead.go new file mode 100644 index 00000000..e96252c1 --- /dev/null +++ b/openpgp/packet/symmetrically_encrypted_aead.go @@ -0,0 +1,156 @@ +// Copyright 2023 Proton AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package packet + +import ( + "crypto/cipher" + "crypto/sha256" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "golang.org/x/crypto/hkdf" +) + +// parseAead parses 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 +func (se *SymmetricallyEncrypted) parseAead(r io.Reader) error { + headerData := make([]byte, 3) + if n, err := io.ReadFull(r, headerData); n < 3 { + return errors.StructuralError("could not read aead header: " + err.Error()) + } + + // Cipher + se.Cipher = CipherFunction(headerData[0]) + // cipherFunc must have block size 16 to use AEAD + if se.Cipher.blockSize() != 16 { + return errors.UnsupportedError("invalid aead cipher: " + string(se.Cipher)) + } + + // Mode + se.Mode = AEADMode(headerData[1]) + if se.Mode.TagLength() == 0 { + return errors.UnsupportedError("unknown aead mode: " + string(se.Mode)) + } + + // Chunk size + se.ChunkSizeByte = headerData[2] + if se.ChunkSizeByte > 16 { + return errors.UnsupportedError("invalid aead chunk size byte: " + string(se.ChunkSizeByte)) + } + + // Salt + if n, err := io.ReadFull(r, se.Salt[:]); n < aeadSaltSize { + return errors.StructuralError("could not read aead salt: " + err.Error()) + } + + return nil +} + +// associatedData for chunks: tag, version, cipher, mode, chunk size byte +func (se *SymmetricallyEncrypted) associatedData() []byte { + return []byte{ + 0xD2, + symmetricallyEncryptedVersionAead, + byte(se.Cipher), + byte(se.Mode), + se.ChunkSizeByte, + } +} + +// 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 +func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, error) { + aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData()) + + // Carry the first tagLen bytes + tagLen := se.Mode.TagLength() + peekedBytes := make([]byte, tagLen) + n, err := io.ReadFull(se.Contents, peekedBytes) + if n < tagLen || (err != nil && err != io.EOF) { + return nil, errors.StructuralError("not enough data to decrypt:" + err.Error()) + } + + return &aeadDecrypter{ + aeadCrypter: aeadCrypter{ + aead: aead, + chunkSize: decodeAEADChunkSize(se.ChunkSizeByte), + initialNonce: nonce, + associatedData: se.associatedData(), + chunkIndex: make([]byte, 8), + packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected, + }, + reader: se.Contents, + peekedBytes: peekedBytes, + }, nil +} + +// serializeSymmetricallyEncryptedAead encrypts to a writer 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 +func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite CipherSuite, chunkSizeByte byte, rand io.Reader, inputKey []byte) (Contents io.WriteCloser, err error) { + // cipherFunc must have block size 16 to use AEAD + if cipherSuite.Cipher.blockSize() != 16 { + return nil, errors.InvalidArgumentError("invalid aead cipher function") + } + + if cipherSuite.Cipher.KeySize() != len(inputKey) { + return nil, errors.InvalidArgumentError("error in aead serialization: bad key length") + } + + // Data for en/decryption: tag, version, cipher, aead mode, chunk size + prefix := []byte{ + 0xD2, + symmetricallyEncryptedVersionAead, + byte(cipherSuite.Cipher), + byte(cipherSuite.Mode), + chunkSizeByte, + } + + // Write header (that correspond to prefix except first byte) + n, err := ciphertext.Write(prefix[1:]) + if err != nil || n < 4 { + return nil, err + } + + // Random salt + salt := make([]byte, aeadSaltSize) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + if _, err := ciphertext.Write(salt); err != nil { + return nil, err + } + + aead, nonce := getSymmetricallyEncryptedAeadInstance(cipherSuite.Cipher, cipherSuite.Mode, inputKey, salt, prefix) + + return &aeadEncrypter{ + aeadCrypter: aeadCrypter{ + aead: aead, + chunkSize: decodeAEADChunkSize(chunkSizeByte), + associatedData: prefix, + chunkIndex: make([]byte, 8), + initialNonce: nonce, + packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected, + }, + writer: ciphertext, + }, nil +} + +func getSymmetricallyEncryptedAeadInstance(c CipherFunction, mode AEADMode, inputKey, salt, associatedData []byte) (aead cipher.AEAD, nonce []byte) { + hkdfReader := hkdf.New(sha256.New, inputKey, salt, associatedData) + + encryptionKey := make([]byte, c.KeySize()) + _, _ = readFull(hkdfReader, encryptionKey) + + // Last 64 bits of nonce are the counter + nonce = make([]byte, mode.IvLength()-8) + + _, _ = readFull(hkdfReader, nonce) + + blockCipher := c.new(encryptionKey) + aead = mode.new(blockCipher) + + return +} diff --git a/openpgp/packet/symmetrically_encrypted_mdc.go b/openpgp/packet/symmetrically_encrypted_mdc.go new file mode 100644 index 00000000..fa26bebe --- /dev/null +++ b/openpgp/packet/symmetrically_encrypted_mdc.go @@ -0,0 +1,256 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package packet + +import ( + "crypto/cipher" + "crypto/sha1" + "crypto/subtle" + "hash" + "io" + "strconv" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +// seMdcReader wraps an io.Reader with a no-op Close method. +type seMdcReader struct { + in io.Reader +} + +func (ser seMdcReader) Read(buf []byte) (int, error) { + return ser.in.Read(buf) +} + +func (ser seMdcReader) Close() error { + return nil +} + +func (se *SymmetricallyEncrypted) decryptMdc(c CipherFunction, key []byte) (io.ReadCloser, error) { + if !c.IsSupported() { + return nil, errors.UnsupportedError("unsupported cipher: " + strconv.Itoa(int(c))) + } + + if len(key) != c.KeySize() { + return nil, errors.InvalidArgumentError("SymmetricallyEncrypted: incorrect key length") + } + + if se.prefix == nil { + se.prefix = make([]byte, c.blockSize()+2) + _, err := readFull(se.Contents, se.prefix) + if err != nil { + return nil, err + } + } else if len(se.prefix) != c.blockSize()+2 { + return nil, errors.InvalidArgumentError("can't try ciphers with different block lengths") + } + + ocfbResync := OCFBResync + if se.IntegrityProtected { + // MDC packets use a different form of OCFB mode. + ocfbResync = OCFBNoResync + } + + s := NewOCFBDecrypter(c.new(key), se.prefix, ocfbResync) + + plaintext := cipher.StreamReader{S: s, R: se.Contents} + + if se.IntegrityProtected { + // IntegrityProtected packets have an embedded hash that we need to check. + h := sha1.New() + h.Write(se.prefix) + return &seMDCReader{in: plaintext, h: h}, nil + } + + // Otherwise, we just need to wrap plaintext so that it's a valid ReadCloser. + return seMdcReader{plaintext}, nil +} + +const mdcTrailerSize = 1 /* tag byte */ + 1 /* length byte */ + sha1.Size + +// An seMDCReader wraps an io.Reader, maintains a running hash and keeps hold +// of the most recent 22 bytes (mdcTrailerSize). Upon EOF, those bytes form an +// MDC packet containing a hash of the previous Contents which is checked +// against the running hash. See RFC 4880, section 5.13. +type seMDCReader struct { + in io.Reader + h hash.Hash + trailer [mdcTrailerSize]byte + scratch [mdcTrailerSize]byte + trailerUsed int + error bool + eof bool +} + +func (ser *seMDCReader) Read(buf []byte) (n int, err error) { + if ser.error { + err = io.ErrUnexpectedEOF + return + } + if ser.eof { + err = io.EOF + return + } + + // If we haven't yet filled the trailer buffer then we must do that + // first. + for ser.trailerUsed < mdcTrailerSize { + n, err = ser.in.Read(ser.trailer[ser.trailerUsed:]) + ser.trailerUsed += n + if err == io.EOF { + if ser.trailerUsed != mdcTrailerSize { + n = 0 + err = io.ErrUnexpectedEOF + ser.error = true + return + } + ser.eof = true + n = 0 + return + } + + if err != nil { + n = 0 + return + } + } + + // If it's a short read then we read into a temporary buffer and shift + // the data into the caller's buffer. + if len(buf) <= mdcTrailerSize { + n, err = readFull(ser.in, ser.scratch[:len(buf)]) + copy(buf, ser.trailer[:n]) + ser.h.Write(buf[:n]) + copy(ser.trailer[:], ser.trailer[n:]) + copy(ser.trailer[mdcTrailerSize-n:], ser.scratch[:]) + if n < len(buf) { + ser.eof = true + err = io.EOF + } + return + } + + n, err = ser.in.Read(buf[mdcTrailerSize:]) + copy(buf, ser.trailer[:]) + ser.h.Write(buf[:n]) + copy(ser.trailer[:], buf[n:]) + + if err == io.EOF { + ser.eof = true + } + return +} + +// This is a new-format packet tag byte for a type 19 (Integrity Protected) packet. +const mdcPacketTagByte = byte(0x80) | 0x40 | 19 + +func (ser *seMDCReader) Close() error { + if ser.error { + return errors.ErrMDCMissing + } + + for !ser.eof { + // We haven't seen EOF so we need to read to the end + var buf [1024]byte + _, err := ser.Read(buf[:]) + if err == io.EOF { + break + } + if err != nil { + return errors.ErrMDCMissing + } + } + + ser.h.Write(ser.trailer[:2]) + + final := ser.h.Sum(nil) + if subtle.ConstantTimeCompare(final, ser.trailer[2:]) != 1 { + return errors.ErrMDCHashMismatch + } + // The hash already includes the MDC header, but we still check its value + // to confirm encryption correctness + if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size { + return errors.ErrMDCMissing + } + return nil +} + +// An seMDCWriter writes through to an io.WriteCloser while maintains a running +// hash of the data written. On close, it emits an MDC packet containing the +// running hash. +type seMDCWriter struct { + w io.WriteCloser + h hash.Hash +} + +func (w *seMDCWriter) Write(buf []byte) (n int, err error) { + w.h.Write(buf) + return w.w.Write(buf) +} + +func (w *seMDCWriter) Close() (err error) { + var buf [mdcTrailerSize]byte + + buf[0] = mdcPacketTagByte + buf[1] = sha1.Size + w.h.Write(buf[:2]) + digest := w.h.Sum(nil) + copy(buf[2:], digest) + + _, err = w.w.Write(buf[:]) + if err != nil { + return + } + return w.w.Close() +} + +// noOpCloser is like an ioutil.NopCloser, but for an io.Writer. +type noOpCloser struct { + w io.Writer +} + +func (c noOpCloser) Write(data []byte) (n int, err error) { + return c.w.Write(data) +} + +func (c noOpCloser) Close() error { + return nil +} + +func serializeSymmetricallyEncryptedMdc(ciphertext io.WriteCloser, c CipherFunction, key []byte, config *Config) (Contents io.WriteCloser, err error) { + // Disallow old cipher suites + if !c.IsSupported() || c < CipherAES128 { + return nil, errors.InvalidArgumentError("invalid mdc cipher function") + } + + if c.KeySize() != len(key) { + return nil, errors.InvalidArgumentError("error in mdc serialization: bad key length") + } + + _, err = ciphertext.Write([]byte{symmetricallyEncryptedVersionMdc}) + if err != nil { + return + } + + block := c.new(key) + blockSize := block.BlockSize() + iv := make([]byte, blockSize) + _, err = config.Random().Read(iv) + if err != nil { + return + } + s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync) + _, err = ciphertext.Write(prefix) + if err != nil { + return + } + plaintext := cipher.StreamWriter{S: s, W: ciphertext} + + h := sha1.New() + h.Write(iv) + h.Write(iv[blockSize-2:]) + Contents = &seMDCWriter{w: plaintext, h: h} + return +} diff --git a/openpgp/packet/symmetrically_encrypted_test.go b/openpgp/packet/symmetrically_encrypted_test.go index 78962499..ef63f8b0 100644 --- a/openpgp/packet/symmetrically_encrypted_test.go +++ b/openpgp/packet/symmetrically_encrypted_test.go @@ -6,12 +6,15 @@ package packet import ( "bytes" + "crypto/rand" "crypto/sha1" "encoding/hex" - "github.com/ProtonMail/go-crypto/openpgp/errors" + goerrors "errors" "io" "io/ioutil" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) // TestReader wraps a []byte and returns reads of a specific length. @@ -28,17 +31,21 @@ func (t *testReader) Read(buf []byte) (n int, err error) { if n > len(buf) { n = len(buf) } - copy(buf, t.data) + + copy(buf[:n], t.data) t.data = t.data[n:] + if len(t.data) == 0 { err = io.EOF } + return } -func testMDCReader(t *testing.T) { - mdcPlaintext, _ := hex.DecodeString(mdcPlaintextHex) +const mdcPlaintextHex = "cb1362000000000048656c6c6f2c20776f726c6421d314c23d643f478a9a2098811fcb191e7b24b80966a1" +func TestMDCReader(t *testing.T) { + mdcPlaintext, _ := hex.DecodeString(mdcPlaintextHex) for stride := 1; stride < len(mdcPlaintext)/2; stride++ { r := &testReader{data: mdcPlaintext, stride: stride} mdcReader := &seMDCReader{in: r, h: sha1.New()} @@ -70,19 +77,22 @@ func testMDCReader(t *testing.T) { err = mdcReader.Close() if err == nil { t.Error("corruption: no error") - } else if _, ok := err.(*errors.SignatureError); !ok { + } else if !goerrors.Is(err, errors.ErrMDCHashMismatch) { t.Errorf("corruption: expected SignatureError, got: %s", err) } } -const mdcPlaintextHex = "a302789c3b2d93c4e0eb9aba22283539b3203335af44a134afb800c849cb4c4de10200aff40b45d31432c80cb384299a0655966d6939dfdeed1dddf980" - -func TestSerialize(t *testing.T) { +func TestSerializeMdc(t *testing.T) { buf := bytes.NewBuffer(nil) c := CipherAES128 key := make([]byte, c.KeySize()) - w, err := SerializeSymmetricallyEncrypted(buf, c, key, nil) + cipherSuite := CipherSuite{ + Cipher: c, + Mode: AEADModeOCB, + } + + w, err := SerializeSymmetricallyEncrypted(buf, c, false, cipherSuite, key, nil) if err != nil { t.Errorf("error from SerializeSymmetricallyEncrypted: %s", err) return @@ -121,3 +131,166 @@ func TestSerialize(t *testing.T) { t.Errorf("contents not equal got: %x want: %x", contentsCopy.Bytes(), contents) } } + +const aeadHexKey = "1936fc8568980274bb900d8319360c77" +const aeadHexSeipd = "d26902070306fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9ffc6fc6d65bbfd24dcd0790966e6d1e85a30053784cb1d8b6a0699ef12155a7b2ad6258531b57651fd7777912fa95e35d9b40216f69a4c248db28ff4331f1632907399e6ff9" +const aeadHexPlainText = "cb1362000000000048656c6c6f2c20776f726c6421d50e1ce2269a9eddef81032172b7ed7c" +const aeadExpectedSalt = "fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9f" + +func TestAeadRfcVector(t *testing.T) { + key, err := hex.DecodeString(aeadHexKey) + if err != nil { + t.Errorf("error in decoding key: %s", err) + } + + packet, err := hex.DecodeString(aeadHexSeipd) + if err != nil { + t.Errorf("error in decoding packet: %s", err) + } + + plainText, err := hex.DecodeString(aeadHexPlainText) + if err != nil { + t.Errorf("error in decoding plaintext: %s", err) + } + + expectedSalt, err := hex.DecodeString(aeadExpectedSalt) + if err != nil { + t.Errorf("error in decoding salt: %s", err) + } + + buf := bytes.NewBuffer(packet) + p, err := Read(buf) + if err != nil { + t.Errorf("error from Read: %s", err) + return + } + + se, ok := p.(*SymmetricallyEncrypted) + if !ok { + t.Errorf("didn't read a *SymmetricallyEncrypted") + return + } + + if se.Version != symmetricallyEncryptedVersionAead { + t.Errorf("found wrong version, want: %d, got: %d", symmetricallyEncryptedVersionAead, se.Version) + } + + if se.Cipher != CipherAES128 { + t.Errorf("found wrong cipher, want: %d, got: %d", CipherAES128, se.Cipher) + } + + if se.Mode != AEADModeGCM { + t.Errorf("found wrong mode, want: %d, got: %d", AEADModeGCM, se.Mode) + } + + if !bytes.Equal(se.Salt[:], expectedSalt) { + t.Errorf("found wrong salt, want: %x, got: %x", expectedSalt, se.Salt) + } + + if se.ChunkSizeByte != 0x06 { + t.Errorf("found wrong chunk size byte, want: %d, got: %d", 0x06, se.ChunkSizeByte) + } + + aeadReader, err := se.Decrypt(CipherFunction(0), key) + if err != nil { + t.Errorf("error from Decrypt: %s", err) + return + } + + decrypted, err := ioutil.ReadAll(aeadReader) + if err != nil { + t.Errorf("error when reading: %s", err) + return + } + + err = aeadReader.Close() + if err != nil { + t.Errorf("error when closing reader: %s", err) + return + } + + if !bytes.Equal(decrypted, plainText) { + t.Errorf("contents not equal got: %x want: %x", decrypted, plainText) + } +} + +func TestAeadEncryptDecrypt(t *testing.T) { + ciphers := map[string]CipherFunction{ + "AES128": CipherAES128, + "AES192": CipherAES192, + "AES256": CipherAES256, + } + + modes := map[string]AEADMode{ + "EAX": AEADModeEAX, + "OCB": AEADModeOCB, + "GCM": AEADModeGCM, + } + + for cipherName, cipher := range ciphers { + t.Run(cipherName, func(t *testing.T) { + for modeName, mode := range modes { + t.Run(modeName, func(t *testing.T) { + testSerializeAead(t, CipherSuite{Cipher: cipher, Mode: mode}) + }) + } + }) + } +} + +func testSerializeAead(t *testing.T, cipherSuite CipherSuite) { + buf := bytes.NewBuffer(nil) + key := make([]byte, cipherSuite.Cipher.KeySize()) + _, _ = rand.Read(key) + + w, err := SerializeSymmetricallyEncrypted(buf, CipherFunction(0), true, cipherSuite, key, &Config{AEADConfig: &AEADConfig{}}) + if err != nil { + t.Errorf("error from SerializeSymmetricallyEncrypted: %s", err) + return + } + + contents := []byte("hello world\n") + + w.Write(contents) + w.Close() + + p, err := Read(buf) + if err != nil { + t.Errorf("error from Read: %s", err) + return + } + + se, ok := p.(*SymmetricallyEncrypted) + if !ok { + t.Errorf("didn't read a *SymmetricallyEncrypted") + return + } + + if se.Version != symmetricallyEncryptedVersionAead { + t.Errorf("found wrong version, want: %d, got: %d", symmetricallyEncryptedVersionAead, se.Version) + } + + if se.Cipher != cipherSuite.Cipher { + t.Errorf("found wrong cipher, want: %d, got: %d", cipherSuite.Cipher, se.Cipher) + } + + if se.Mode != cipherSuite.Mode { + t.Errorf("found wrong mode, want: %d, got: %d", cipherSuite.Mode, se.Mode) + } + + r, err := se.Decrypt(CipherFunction(0), key) + if err != nil { + t.Errorf("error from Decrypt: %s", err) + return + } + + contentsCopy := bytes.NewBuffer(nil) + _, err = io.Copy(contentsCopy, r) + if err != nil { + t.Errorf("error from io.Copy: %s", err) + return + } + if !bytes.Equal(contentsCopy.Bytes(), contents) { + t.Errorf("contents not equal got: %x want: %x", contentsCopy.Bytes(), contents) + } +} diff --git a/openpgp/read.go b/openpgp/read.go index bfc897cc..8499c737 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -15,6 +15,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" _ "golang.org/x/crypto/sha3" ) @@ -133,8 +134,8 @@ ParsePackets: } } case *packet.SymmetricallyEncrypted: - if !p.MDC && !config.AllowUnauthenticatedMessages() { - return nil, errors.UnsupportedError("message is not authenticated") + if !p.IntegrityProtected && !config.AllowUnauthenticatedMessages() { + return nil, errors.UnsupportedError("message is not integrity protected") } edp = p break ParsePackets @@ -210,13 +211,11 @@ FindKey: if len(symKeys) != 0 && passphrase != nil { for _, s := range symKeys { key, cipherFunc, err := s.Decrypt(passphrase) - // On wrong passphrase, session key decryption is very likely to result in an invalid cipherFunc: + // In v4, on wrong passphrase, session key decryption is very likely to result in an invalid cipherFunc: // only for < 5% of cases we will proceed to decrypt the data if err == nil { decrypted, err = edp.Decrypt(cipherFunc, key) - // TODO: ErrKeyIncorrect is no longer thrown on SEIP decryption, - // but it might still be relevant for when we implement AEAD decryption (otherwise, remove?) - if err != nil && err != errors.ErrKeyIncorrect { + if err != nil { return nil, err } if decrypted != nil { @@ -306,14 +305,14 @@ FindLiteralData: // 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 // performs any needed preprocessing. -func hashForSignature(hashId crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) { - if hashId == crypto.MD5 { - return nil, nil, errors.UnsupportedError("insecure hash algorithm: MD5") +func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) { + if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok { + return nil, nil, errors.UnsupportedError("unsupported hash function") } - if !hashId.Available() { - return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashId))) + if !hashFunc.Available() { + return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc))) } - h := hashId.New() + h := hashFunc.New() switch sigType { case packet.SigTypeBinary: @@ -385,19 +384,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) { key := scr.md.SignedBy signatureError := key.PublicKey.VerifySignature(scr.h, sig) if signatureError == nil { - now := scr.config.Now() - if key.Revoked(now) || - key.Entity.Revoked(now) || // primary key is revoked (redundant if key is the primary key) - key.Entity.PrimaryIdentity().Revoked(now) { - signatureError = errors.ErrKeyRevoked - } - if sig.SigExpired(now) { - signatureError = errors.ErrSignatureExpired - } - if key.PublicKey.KeyExpired(key.SelfSignature, now) || - key.SelfSignature.SigExpired(now) { - signatureError = errors.ErrKeyExpired - } + signatureError = checkSignatureDetails(key, sig, scr.config) } scr.md.Signature = sig scr.md.SignatureError = signatureError @@ -436,8 +423,24 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) { return n, nil } +// VerifyDetachedSignature takes a signed file and a detached signature and +// returns the signature packet and the entity the signature was signed by, +// if any, and a possible signature verification error. +// 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) { + var expectedHashes []crypto.Hash + return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) +} + +// VerifyDetachedSignatureAndHash performs the same actions as +// 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) { + return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) +} + // CheckDetachedSignature takes a signed file and a detached signature and -// returns the signer if the signature is valid. If the signer isn't known, +// returns the entity the signature was signed by, if any, and a possible +// signature verification error. If the signer isn't known, // ErrUnknownIssuer is returned. func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (signer *Entity, err error) { var expectedHashes []crypto.Hash @@ -447,6 +450,11 @@ func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config // CheckDetachedSignatureAndHash performs the same actions as // 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) { + _, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) + return +} + +func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { var issuerKeyId uint64 var hashFunc crypto.Hash var sigType packet.SignatureType @@ -455,23 +463,22 @@ func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashesLen := len(expectedHashes) packets := packet.NewReader(signature) - var sig *packet.Signature for { p, err = packets.Next() if err == io.EOF { - return nil, errors.ErrUnknownIssuer + return nil, nil, errors.ErrUnknownIssuer } if err != nil { - return nil, err + return nil, nil, err } var ok bool sig, ok = p.(*packet.Signature) if !ok { - return nil, errors.StructuralError("non signature packet found") + return nil, nil, errors.StructuralError("non signature packet found") } if sig.IssuerKeyId == nil { - return nil, errors.StructuralError("signature doesn't have an issuer") + return nil, nil, errors.StructuralError("signature doesn't have an issuer") } issuerKeyId = *sig.IssuerKeyId hashFunc = sig.Hash @@ -482,7 +489,7 @@ func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, break } if i+1 == expectedHashesLen { - return nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers") + return nil, nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers") } } @@ -498,34 +505,21 @@ func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, h, wrappedHash, err := hashForSignature(hashFunc, sigType) if err != nil { - return nil, err + return nil, nil, err } if _, err := io.Copy(wrappedHash, signed); err != nil && err != io.EOF { - return nil, err + return nil, nil, err } for _, key := range keys { err = key.PublicKey.VerifySignature(h, sig) if err == nil { - now := config.Now() - if key.Revoked(now) || - key.Entity.Revoked(now) || // primary key is revoked (redundant if key is the primary key) - key.Entity.PrimaryIdentity().Revoked(now) { - return key.Entity, errors.ErrKeyRevoked - } - if sig.SigExpired(now) { - return key.Entity, errors.ErrSignatureExpired - } - if key.PublicKey.KeyExpired(key.SelfSignature, now) || - key.SelfSignature.SigExpired(now) { - return key.Entity, errors.ErrKeyExpired - } - return key.Entity, nil + return sig, key.Entity, checkSignatureDetails(&key, sig, config) } } - return nil, err + return nil, nil, err } // CheckArmoredDetachedSignature performs the same actions as @@ -538,3 +532,61 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader, return CheckDetachedSignature(keyring, signed, body, config) } + +// checkSignatureDetails returns an error if: +// - The signature (or one of the binding signatures mentioned below) +// has a unknown critical notation data subpacket +// - The primary key of the signing entity is revoked +// - The primary identity is revoked +// - The signature is expired +// - The primary key of the signing entity is expired according to the +// primary identity binding signature +// +// ... or, if the signature was signed by a subkey and: +// - The signing subkey is revoked +// - The signing subkey is expired according to the subkey binding signature +// - The signing subkey binding signature is expired +// - The signing subkey cross-signature is expired +// +// NOTE: The order of these checks is important, as the caller may choose to +// ignore ErrSignatureExpired or ErrKeyExpired errors, but should never +// ignore any other errors. +// +// 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() + primaryIdentity := key.Entity.PrimaryIdentity() + signedBySubKey := key.PublicKey != key.Entity.PrimaryKey + sigsToCheck := []*packet.Signature{signature, primaryIdentity.SelfSignature} + if signedBySubKey { + sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature) + } + for _, sig := range sigsToCheck { + for _, notation := range sig.Notations { + if notation.IsCritical && !config.KnownNotation(notation.Name) { + return errors.SignatureError("unknown critical notation: " + notation.Name) + } + } + } + if key.Entity.Revoked(now) || // primary key is revoked + (signedBySubKey && key.Revoked(now)) || // subkey is revoked + primaryIdentity.Revoked(now) { // primary identity is revoked + return errors.ErrKeyRevoked + } + if key.Entity.PrimaryKey.KeyExpired(primaryIdentity.SelfSignature, now) { // primary key is expired + return errors.ErrKeyExpired + } + if signedBySubKey { + if key.PublicKey.KeyExpired(key.SelfSignature, now) { // subkey is expired + return errors.ErrKeyExpired + } + } + for _, sig := range sigsToCheck { + if sig.SigExpired(now) { // any of the relevant signatures are expired + return errors.ErrSignatureExpired + } + } + return nil +} diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 664a939d..bffa1c53 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -353,7 +353,7 @@ func testDetachedSignature(t *testing.T, kring KeyRing, signature io.Reader, sig return } if signer.PrimaryKey.KeyId != expectedSignerKeyId { - t.Errorf("%s: wrong signer got:%x want:%x", tag, signer.PrimaryKey.KeyId, expectedSignerKeyId) + t.Errorf("%s: wrong signer: got %x, expected %x", tag, signer.PrimaryKey.KeyId, expectedSignerKeyId) } } @@ -423,6 +423,75 @@ func TestRSASignatureBadMPILength(t *testing.T) { } } +func TestDetachedSignatureExpiredCrossSig(t *testing.T) { + kring, _ := ReadArmoredKeyRing(bytes.NewBufferString(keyWithExpiredCrossSig)) + config := &packet.Config{} + _, err := CheckArmoredDetachedSignature(kring, bytes.NewBufferString("Hello World :)"), bytes.NewBufferString(sigFromKeyWithExpiredCrossSig), config) + if err == nil { + t.Fatal("Signature from key with expired subkey binding embedded signature was accepted") + } + if err != errors.ErrSignatureExpired { + t.Fatalf("Unexpected class of error: %s", err) + } +} + +func TestSignatureUnknownNotation(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(criticalNotationSigner)) + if err != nil { + t.Error(err) + } + raw, err := armor.Decode(strings.NewReader(signedMessageWithCriticalNotation)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(raw.Body, el, nil, nil) + if err != nil { + t.Error(err) + return + } + _, err = ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + const expectedErr string = "openpgp: invalid signature: unknown critical notation: test@example.com" + if md.SignatureError == nil || md.SignatureError.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, md.SignatureError) + } +} + +func TestSignatureKnownNotation(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(criticalNotationSigner)) + if err != nil { + t.Error(err) + } + raw, err := armor.Decode(strings.NewReader(signedMessageWithCriticalNotation)) + if err != nil { + t.Error(err) + return + } + config := &packet.Config{ + KnownNotations: map[string]bool{ + "test@example.com": true, + }, + } + md, err := ReadMessage(raw.Body, el, nil, config) + if err != nil { + t.Error(err) + return + } + _, err = ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if md.SignatureError != nil { + t.Error(md.SignatureError) + return + } +} + func TestReadingArmoredPrivateKey(t *testing.T) { el, err := ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKeyBlock)) if err != nil { @@ -519,9 +588,10 @@ func TestSignatureV3Message(t *testing.T) { return } -func TestSymmetricAeadGcmOpenPGPJsMessage(t *testing.T) { - passphrase := []byte("test") - file, err := os.Open("test_data/aead-sym-message.asc") +func TestSymmetricDecryptionArgon2(t *testing.T) { + // Appendix IETF OpenPGP crypto refresh draft v08 A.8.1 + passphrase := []byte("password") + file, err := os.Open("test_data/argon2-sym-message.asc") if err != nil { t.Fatal(err) } @@ -550,14 +620,8 @@ func TestSymmetricAeadGcmOpenPGPJsMessage(t *testing.T) { t.Errorf("error reading UnverifiedBody: %s", err) } - // The plaintext is https://www.gutenberg.org/cache/epub/1080/pg1080.txt - // We compare the SHA512 hashes. - wantHash := modestProposalSha512 - gotHashRaw := sha512.Sum512(contents) - gotHash := base64.StdEncoding.EncodeToString(gotHashRaw[:]) - - if wantHash != gotHash { - t.Fatal("Did not decrypt OpenPGPjs message correctly") + if "Hello, world!" != string(contents) { + t.Fatal("Did not decrypt Argon message correctly") } } diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index ab12d576..570eec4c 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -1,8 +1,8 @@ package openpgp -const testKey1KeyId = 0xA34D7E18C20C31BB -const testKey3KeyId = 0x338934250CCC0360 -const testKeyP256KeyId = 0xd44a2c495918513e +const testKey1KeyId uint64 = 0xA34D7E18C20C31BB +const testKey3KeyId uint64 = 0x338934250CCC0360 +const testKeyP256KeyId uint64 = 0xd44a2c495918513e const signedInput = "Signed message\nline 2\nline 3\n" const signedTextInput = "Signed message\r\nline 2\r\nline 3\r\n" @@ -106,7 +106,7 @@ const unknownHashFunctionHex = `8a00000040040001990006050253863c24000a09103b4fe6 const rsaSignatureBadMPIlength = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` -const missingHashFunctionHex = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101` +const missingHashFunctionHex = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` const campbellQuine = `a0b001000300fcffa0b001000d00f2ff000300fcffa0b001000d00f2ff8270a01c00000500faff8270a01c00000500faff000500faff001400ebff8270a01c00000500faff000500faff001400ebff428821c400001400ebff428821c400001400ebff428821c400001400ebff428821c400001400ebff428821c400000000ffff000000ffff000b00f4ff428821c400000000ffff000000ffff000b00f4ff0233214c40000100feff000233214c40000100feff0000` @@ -172,6 +172,7 @@ UQdl5MlBka1QSNbMq2Bz7XwNPg4= =6lbM -----END PGP MESSAGE-----` + // Generated with the above private key const pgpPhotoPublicOpenPGP2_2_34 = `-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -865,3 +866,105 @@ z829BTssIwEA8JPOgk+yssCWu08ksLEY9rvQrVX6cQuSg2KihdLM3gQ= =vfpo -----END PGP PUBLIC KEY BLOCK----- ` + +const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +hW1Hj9AO8lzggBQ= +=Nt+N +-----END PGP PUBLIC KEY BLOCK----- +` + +const sigFromKeyWithExpiredCrossSig = `-----BEGIN PGP SIGNATURE----- + +wsDzBAABCgAGBYJfLAFsACEJEHwvqk35PDeyFiEEHdzhXwkhfO4vOzdgfC+qTfk8 +N7KiqwwAts4QGB7v9bABCC2qkTxJhmStC0wQMcHRcjL/qAiVnmasQWmvE9KVsdm3 +AaXd8mIx4a37/RRvr9dYrY2eE4uw72cMqPxNja2tvVXkHQvk1oEUqfkvbXs4ypKI +NyeTWjXNOTZEbg0hbm3nMy+Wv7zgB1CEvAsEboLDJlhGqPcD+X8a6CJGrBGUBUrv +KVmZr3U6vEzClz3DBLpoddCQseJRhT4YM1nKmBlZ5quh2LFgTSpajv5OsZheqt9y +EZAPbqmLhDmWRQwGzkWHKceKS7nZ/ox2WK6OS7Ob8ZGZkM64iPo6/EGj5Yc19vQN +AGiIaPEGszBBWlOpHTPhNm0LB0nMWqqaT87oNYwP8CQuuxDb6rKJ2lffCmZH27Lb +UbQZcH8J+0UhpeaiadPZxH5ATJAcenmVtVVMLVOFnm+eIlxzov9ntpgGYt8hLdXB +ITEG9mMgp3TGS9ZzSifMZ8UGtHdp9QdBg8NEVPFzDOMGxpc/Bftav7RRRuPiAER+ +7A5CBid5 +=aQkm +-----END PGP SIGNATURE----- +` + +const signedMessageWithCriticalNotation = `-----BEGIN PGP MESSAGE----- + +owGbwMvMwMH4oOW7S46CznTG09xJDDE3Wl1KUotLuDousDAwcjBYiSmyXL+48d6x +U1PSGUxcj8IUszKBVMpMaWAAAgEGZpAeh9SKxNyCnFS95PzcytRiBi5OAZjyXXzM +f8WYLqv7TXP61Sa4rqT12CI3xaN73YS2pt089f96odCKaEPnWJ3iSGmzJaW/ug10 +2Zo8Wj2k4s7t8wt4H3HtTu+y5UZfV3VOO+l//sdE/o+Lsub8FZH7/eOq7OnbNp4n +vwjE8mqJXetNMfj8r2SCyvkEnlVRYR+/mnge+ib56FdJ8uKtqSxyvgA= +=fRXs +-----END PGP MESSAGE-----` + +const criticalNotationSigner = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+ +fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5 +GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0 +JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS +YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6 +AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki +Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf +9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rC4jQRSYS9OAQQA6R/PtBFa +JaT4jq10yqASk4sqwVMsc6HcifM5lSdxzExFP74naUMMyEsKHP53QxTF0Grqusag +Qg/ZtgT0CN1HUM152y7ACOdp1giKjpMzOTQClqCoclyvWOFB+L/SwGEIJf7LSCEr +woBuJifJc8xAVr0XX0JthoW+uP91eTQ3XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIb +LgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJSYS9OAAoJEOCE90RsICyXuqIEANmmiRCA +SF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhP +GLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2 +bZpsUxRTg9+cSrMWWSNjiY9qUKajm1tuzPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0X +W748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxqE9+QCEl7qinFqqBLjuzgUhBU4QlwX1GD +AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY +hz3tYjKhoFTKEIq3y3Pp +=h/aX +-----END PGP PUBLIC KEY BLOCK-----` + diff --git a/openpgp/s2k/s2k.go b/openpgp/s2k/s2k.go index 8862c7c2..a4369596 100644 --- a/openpgp/s2k/s2k.go +++ b/openpgp/s2k/s2k.go @@ -3,7 +3,8 @@ // license that can be found in the LICENSE file. // Package s2k implements the various OpenPGP string-to-key transforms as -// specified in RFC 4800 section 3.7.1. +// specified in RFC 4800 section 3.7.1, and Argon2 specified in +// draft-ietf-openpgp-crypto-refresh-08 section 3.7.1.4. package s2k // import "github.com/ProtonMail/go-crypto/openpgp/s2k" import ( @@ -14,70 +15,47 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "golang.org/x/crypto/argon2" ) -// Config collects configuration parameters for s2k key-stretching -// transformations. A nil *Config is valid and results in all default -// values. Currently, Config is used only by the Serialize function in -// this package. -type Config struct { - // S2KMode is the mode of s2k function. - // It can be 0 (simple), 1(salted), 3(iterated) - // 2(reserved) 100-110(private/experimental). - S2KMode uint8 - // Hash is the default hash function to be used. If - // nil, SHA256 is used. - Hash crypto.Hash - // S2KCount is only used for symmetric encryption. It - // determines the strength of the passphrase stretching when - // the said passphrase is hashed to produce a key. S2KCount - // should be between 65536 and 65011712, inclusive. If Config - // is nil or S2KCount is 0, the value 16777216 used. Not all - // values in the above range can be represented. S2KCount will - // be rounded up to the next representable value if it cannot - // be encoded exactly. See RFC 4880 Section 3.7.1.3. - S2KCount int -} +type Mode uint8 + +// Defines the default S2KMode constants +// +// 0 (simple), 1(salted), 3(iterated), 4(argon2) +const ( + SimpleS2K Mode = 0 + SaltedS2K Mode = 1 + IteratedSaltedS2K Mode = 3 + Argon2S2K Mode = 4 + GnuS2K Mode = 101 +) + +const Argon2SaltSize int = 16 // Params contains all the parameters of the s2k packet type Params struct { // mode is the mode of s2k function. // It can be 0 (simple), 1(salted), 3(iterated) // 2(reserved) 100-110(private/experimental). - mode uint8 + mode Mode // hashId is the ID of the hash function used in any of the modes hashId byte - // salt is a byte array to use as a salt in hashing process - salt []byte + // salt is a byte array to use as a salt in hashing process or argon2 + saltBytes [Argon2SaltSize]byte // countByte is used to determine how many rounds of hashing are to // be performed in s2k mode 3. See RFC 4880 Section 3.7.1.3. countByte byte -} - -func (c *Config) hash() crypto.Hash { - if c == nil || uint(c.Hash) == 0 { - return crypto.SHA256 - } - - return c.Hash -} - -// EncodedCount get encoded count -func (c *Config) EncodedCount() uint8 { - if c == nil || c.S2KCount == 0 { - return 224 // The common case. Corresponding to 16777216 - } - - i := c.S2KCount - - switch { - case i < 65536: - i = 65536 - case i > 65011712: - i = 65011712 - } - - return encodeCount(i) + // passes is a parameter in Argon2 to determine the number of iterations + // See RFC the crypto refresh Section 3.7.1.4. + passes byte + // parallelism is a parameter in Argon2 to determine the degree of paralellism + // See RFC the crypto refresh Section 3.7.1.4. + parallelism byte + // memoryExp is a parameter in Argon2 to determine the memory usage + // i.e., 2 ** memoryExp kibibytes + // See RFC the crypto refresh Section 3.7.1.4. + memoryExp byte } // encodeCount converts an iterative "count" in the range 1024 to @@ -106,6 +84,31 @@ func decodeCount(c uint8) int { return (16 + int(c&15)) << (uint32(c>>4) + 6) } +// encodeMemory converts the Argon2 "memory" in the range parallelism*8 to +// 2**31, inclusive, to an encoded memory. The return value is the +// octet that is actually stored in the GPG file. encodeMemory panics +// if is not in the above range +// See OpenPGP crypto refresh Section 3.7.1.4. +func encodeMemory(memory uint32, parallelism uint8) uint8 { + if memory < (8 * uint32(parallelism)) || memory > uint32(2147483648) { + panic("Memory argument memory is outside the required range") + } + + for exp := 3; exp < 31; exp++ { + compare := decodeMemory(uint8(exp)) + if compare >= memory { + return uint8(exp) + } + } + + return 31 +} + +// decodeMemory computes the decoded memory in kibibytes as 2**memoryExponent +func decodeMemory(memoryExponent uint8) uint32 { + return uint32(1) << memoryExponent +} + // Simple writes to out the result of computing the Simple S2K function (RFC // 4880, section 3.7.1.1) using the given hash and input passphrase. func Simple(out []byte, h hash.Hash, in []byte) { @@ -169,25 +172,53 @@ func Iterated(out []byte, h hash.Hash, in []byte, salt []byte, count int) { } } +// Argon2 writes to out the key derived from the password (in) with the Argon2 +// function (the crypto refresh, section 3.7.1.4) +func Argon2(out []byte, in []byte, salt []byte, passes uint8, paralellism uint8, memoryExp uint8) { + key := argon2.IDKey(in, salt, uint32(passes), decodeMemory(memoryExp), paralellism, uint32(len(out))) + copy(out[:], key) +} + // Generate generates valid parameters from given configuration. -// It will enforce salted + hashed s2k method +// It will enforce the Iterated and Salted or Argon2 S2K method. func Generate(rand io.Reader, c *Config) (*Params, error) { - hashId, ok := HashToHashId(c.Hash) - if !ok { - return nil, errors.UnsupportedError("no such hash") - } + var params *Params + if c != nil && c.Mode() == Argon2S2K { + // handle Argon2 case + argonConfig := c.Argon2() + params = &Params{ + mode: Argon2S2K, + passes: argonConfig.Passes(), + parallelism: argonConfig.Parallelism(), + memoryExp: argonConfig.EncodedMemory(), + } + } else if c != nil && c.PassphraseIsHighEntropy && c.Mode() == SaltedS2K { // Allow SaltedS2K if PassphraseIsHighEntropy + hashId, ok := algorithm.HashToHashId(c.hash()) + if !ok { + return nil, errors.UnsupportedError("no such hash") + } - params := &Params{ - mode: 3, // Enforce iterared + salted method - hashId: hashId, - salt: make([]byte, 8), - countByte: c.EncodedCount(), + params = &Params{ + mode: SaltedS2K, + hashId: hashId, + } + } else { // Enforce IteratedSaltedS2K method otherwise + hashId, ok := algorithm.HashToHashId(c.hash()) + if !ok { + return nil, errors.UnsupportedError("no such hash") + } + if c != nil { + c.S2KMode = IteratedSaltedS2K + } + params = &Params{ + mode: IteratedSaltedS2K, + hashId: hashId, + countByte: c.EncodedCount(), + } } - - if _, err := io.ReadFull(rand, params.salt); err != nil { + if _, err := io.ReadFull(rand, params.salt()); err != nil { return nil, err } - return params, nil } @@ -207,45 +238,60 @@ func Parse(r io.Reader) (f func(out, in []byte), err error) { // ParseIntoParams reads a binary specification for a string-to-key // transformation from r and returns a struct describing the s2k parameters. func ParseIntoParams(r io.Reader) (params *Params, err error) { - var buf [9]byte + var buf [Argon2SaltSize + 3]byte - _, err = io.ReadFull(r, buf[:2]) + _, err = io.ReadFull(r, buf[:1]) if err != nil { return } params = &Params{ - mode: buf[0], - hashId: buf[1], + mode: Mode(buf[0]), } switch params.mode { - case 0: - return params, nil - case 1: - _, err = io.ReadFull(r, buf[:8]) + case SimpleS2K: + _, err = io.ReadFull(r, buf[:1]) if err != nil { return nil, err } - - params.salt = buf[:8] + params.hashId = buf[0] return params, nil - case 3: + case SaltedS2K: _, err = io.ReadFull(r, buf[:9]) if err != nil { return nil, err } - - params.salt = buf[:8] - params.countByte = buf[8] + params.hashId = buf[0] + copy(params.salt(), buf[1:9]) + return params, nil + case IteratedSaltedS2K: + _, err = io.ReadFull(r, buf[:10]) + if err != nil { + return nil, err + } + params.hashId = buf[0] + copy(params.salt(), buf[1:9]) + params.countByte = buf[9] + return params, nil + case Argon2S2K: + _, err = io.ReadFull(r, buf[:Argon2SaltSize+3]) + if err != nil { + return nil, err + } + copy(params.salt(), buf[:Argon2SaltSize]) + params.passes = buf[Argon2SaltSize] + params.parallelism = buf[Argon2SaltSize+1] + params.memoryExp = buf[Argon2SaltSize+2] return params, nil - case 101: + case GnuS2K: // This is a GNU extension. See // https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=fe55ae16ab4e26d8356dc574c9e8bc935e71aef1;hb=23191d7851eae2217ecdac6484349849a24fd94a#l1109 - if _, err = io.ReadFull(r, buf[:4]); err != nil { + if _, err = io.ReadFull(r, buf[:5]); err != nil { return nil, err } - if buf[0] == 'G' && buf[1] == 'N' && buf[2] == 'U' && buf[3] == 1 { + params.hashId = buf[0] + if buf[1] == 'G' && buf[2] == 'N' && buf[3] == 'U' && buf[4] == 1 { return params, nil } return nil, errors.UnsupportedError("GNU S2K extension") @@ -255,39 +301,56 @@ func ParseIntoParams(r io.Reader) (params *Params, err error) { } func (params *Params) Dummy() bool { - return params != nil && params.mode == 101 + return params != nil && params.mode == GnuS2K +} + +func (params *Params) salt() []byte { + switch params.mode { + case SaltedS2K, IteratedSaltedS2K: return params.saltBytes[:8] + case Argon2S2K: return params.saltBytes[:Argon2SaltSize] + default: return nil + } } func (params *Params) Function() (f func(out, in []byte), err error) { if params.Dummy() { return nil, errors.ErrDummyPrivateKey("dummy key found") } - hashObj, ok := HashIdToHash(params.hashId) - if !ok { - return nil, errors.UnsupportedError("hash for S2K function: " + strconv.Itoa(int(params.hashId))) - } - if !hashObj.Available() { - return nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashObj))) + var hashObj crypto.Hash + if params.mode != Argon2S2K { + var ok bool + hashObj, ok = algorithm.HashIdToHashWithSha1(params.hashId) + if !ok { + return nil, errors.UnsupportedError("hash for S2K function: " + strconv.Itoa(int(params.hashId))) + } + if !hashObj.Available() { + return nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashObj))) + } } switch params.mode { - case 0: + case SimpleS2K: f := func(out, in []byte) { Simple(out, hashObj.New(), in) } return f, nil - case 1: + case SaltedS2K: f := func(out, in []byte) { - Salted(out, hashObj.New(), in, params.salt) + Salted(out, hashObj.New(), in, params.salt()) } return f, nil - case 3: + case IteratedSaltedS2K: f := func(out, in []byte) { - Iterated(out, hashObj.New(), in, params.salt, decodeCount(params.countByte)) + Iterated(out, hashObj.New(), in, params.salt(), decodeCount(params.countByte)) } + return f, nil + case Argon2S2K: + f := func(out, in []byte) { + Argon2(out, in, params.salt(), params.passes, params.parallelism, params.memoryExp) + } return f, nil } @@ -295,23 +358,28 @@ func (params *Params) Function() (f func(out, in []byte), err error) { } func (params *Params) Serialize(w io.Writer) (err error) { - if _, err = w.Write([]byte{params.mode}); err != nil { + if _, err = w.Write([]byte{uint8(params.mode)}); err != nil { return } - if _, err = w.Write([]byte{params.hashId}); err != nil { - return + if params.mode != Argon2S2K { + if _, err = w.Write([]byte{params.hashId}); err != nil { + return + } } if params.Dummy() { _, err = w.Write(append([]byte("GNU"), 1)) return } if params.mode > 0 { - if _, err = w.Write(params.salt); err != nil { + if _, err = w.Write(params.salt()); err != nil { return } - if params.mode == 3 { + if params.mode == IteratedSaltedS2K { _, err = w.Write([]byte{params.countByte}) } + if params.mode == Argon2S2K { + _, err = w.Write([]byte{params.passes, params.parallelism, params.memoryExp}) + } } return } @@ -337,31 +405,3 @@ func Serialize(w io.Writer, key []byte, rand io.Reader, passphrase []byte, c *Co f(key, passphrase) return nil } - -// HashIdToHash returns a crypto.Hash which corresponds to the given OpenPGP -// hash id. -func HashIdToHash(id byte) (h crypto.Hash, ok bool) { - if hash, ok := algorithm.HashById[id]; ok { - return hash.HashFunc(), true - } - return 0, false -} - -// HashIdToString returns the name of the hash function corresponding to the -// given OpenPGP hash id. -func HashIdToString(id byte) (name string, ok bool) { - if hash, ok := algorithm.HashById[id]; ok { - return hash.String(), true - } - return "", false -} - -// HashToHashId returns an OpenPGP hash id which corresponds the given Hash. -func HashToHashId(h crypto.Hash) (id byte, ok bool) { - for id, hash := range algorithm.HashById { - if hash.HashFunc() == h { - return id, true - } - } - return 0, false -} diff --git a/openpgp/s2k/s2k_cache.go b/openpgp/s2k/s2k_cache.go new file mode 100644 index 00000000..25a4442d --- /dev/null +++ b/openpgp/s2k/s2k_cache.go @@ -0,0 +1,26 @@ +package s2k + +// Cache stores keys derived with s2k functions from one passphrase +// to avoid recomputation if multiple items are encrypted with +// the same parameters. +type Cache map[Params][]byte + +// GetOrComputeDerivedKey tries to retrieve the key +// for the given s2k parameters from the cache. +// If there is no hit, it derives the key with the s2k function from the passphrase, +// updates the cache, and returns the key. +func (c *Cache) GetOrComputeDerivedKey(passphrase []byte, params *Params, expectedKeySize int) ([]byte, error) { + key, found := (*c)[*params] + if !found || len(key) != expectedKeySize { + var err error + derivedKey := make([]byte, expectedKeySize) + s2k, err := params.Function() + if err != nil { + return nil, err + } + s2k(derivedKey, passphrase) + (*c)[*params] = key + return derivedKey, nil + } + return key, nil +} diff --git a/openpgp/s2k/s2k_config.go b/openpgp/s2k/s2k_config.go new file mode 100644 index 00000000..b40be522 --- /dev/null +++ b/openpgp/s2k/s2k_config.go @@ -0,0 +1,129 @@ +package s2k + +import "crypto" + +// Config collects configuration parameters for s2k key-stretching +// transformations. A nil *Config is valid and results in all default +// values. +type Config struct { + // S2K (String to Key) mode, used for key derivation in the context of secret key encryption + // and passphrase-encrypted data. Either s2k.Argon2S2K or s2k.IteratedSaltedS2K may be used. + // If the passphrase is a high-entropy key, indicated by setting PassphraseIsHighEntropy to true, + // s2k.SaltedS2K can also be used. + // Note: Argon2 is the strongest option but not all OpenPGP implementations are compatible with it + //(pending standardisation). + // 0 (simple), 1(salted), 3(iterated), 4(argon2) + // 2(reserved) 100-110(private/experimental). + S2KMode Mode + // Only relevant if S2KMode is not set to s2k.Argon2S2K. + // Hash is the default hash function to be used. If + // nil, SHA256 is used. + Hash crypto.Hash + // Argon2 parameters for S2K (String to Key). + // Only relevant if S2KMode is set to s2k.Argon2S2K. + // If nil, default parameters are used. + // For more details on the choice of parameters, see https://tools.ietf.org/html/rfc9106#section-4. + Argon2Config *Argon2Config + // Only relevant if S2KMode is set to s2k.IteratedSaltedS2K. + // Iteration count for Iterated S2K (String to Key). It + // determines the strength of the passphrase stretching when + // the said passphrase is hashed to produce a key. S2KCount + // should be between 65536 and 65011712, inclusive. If Config + // is nil or S2KCount is 0, the value 16777216 used. Not all + // values in the above range can be represented. S2KCount will + // be rounded up to the next representable value if it cannot + // be encoded exactly. When set, it is strongly encrouraged to + // use a value that is at least 65536. See RFC 4880 Section + // 3.7.1.3. + S2KCount int + // Indicates whether the passphrase passed by the application is a + // high-entropy key (e.g. it's randomly generated or derived from + // another passphrase using a strong key derivation function). + // When true, allows the S2KMode to be s2k.SaltedS2K. + // When the passphrase is not a high-entropy key, using SaltedS2K is + // insecure, and not allowed by draft-ietf-openpgp-crypto-refresh-08. + PassphraseIsHighEntropy bool +} + +// Argon2Config stores the Argon2 parameters +// A nil *Argon2Config is valid and results in all default +type Argon2Config struct { + NumberOfPasses uint8 + DegreeOfParallelism uint8 + // The memory parameter for Argon2 specifies desired memory usage in kibibytes. + // For example memory=64*1024 sets the memory cost to ~64 MB. + Memory uint32 +} + +func (c *Config) Mode() Mode { + if c == nil { + return IteratedSaltedS2K + } + return c.S2KMode +} + +func (c *Config) hash() crypto.Hash { + if c == nil || uint(c.Hash) == 0 { + return crypto.SHA256 + } + + return c.Hash +} + +func (c *Config) Argon2() *Argon2Config { + if c == nil || c.Argon2Config == nil { + return nil + } + return c.Argon2Config +} + +// EncodedCount get encoded count +func (c *Config) EncodedCount() uint8 { + if c == nil || c.S2KCount == 0 { + return 224 // The common case. Corresponding to 16777216 + } + + i := c.S2KCount + + switch { + case i < 65536: + i = 65536 + case i > 65011712: + i = 65011712 + } + + return encodeCount(i) +} + +func (c *Argon2Config) Passes() uint8 { + if c == nil || c.NumberOfPasses == 0 { + return 3 + } + return c.NumberOfPasses +} + +func (c *Argon2Config) Parallelism() uint8 { + if c == nil || c.DegreeOfParallelism == 0 { + return 4 + } + return c.DegreeOfParallelism +} + +func (c *Argon2Config) EncodedMemory() uint8 { + if c == nil || c.Memory == 0 { + return 16 // 64 MiB of RAM + } + + memory := c.Memory + lowerBound := uint32(c.Parallelism())*8 + upperBound := uint32(2147483648) + + switch { + case memory < lowerBound: + memory = lowerBound + case memory > upperBound: + memory = upperBound + } + + return encodeMemory(memory, c.Parallelism()) +} diff --git a/openpgp/s2k/s2k_test.go b/openpgp/s2k/s2k_test.go index c4be8bcb..f89b9694 100644 --- a/openpgp/s2k/s2k_test.go +++ b/openpgp/s2k/s2k_test.go @@ -43,6 +43,32 @@ func TestSalted(t *testing.T) { } } +var argon2EncodeTest = []struct { + in uint32 + out uint8 +}{ + {64*1024, 16}, + {64*1024+1, 17}, + {2147483647, 31}, + {2147483649, 31}, + {1, 3}, +} + +func TestArgon2EncodeTest(t *testing.T) { + + for i, tests := range argon2EncodeTest { + conf := &Argon2Config { + Memory: tests.in, + DegreeOfParallelism: 1, + } + out := conf.EncodedMemory() + if out != tests.out { + t.Errorf("#%d, got: %x want: %x", i, out, tests.out) + } + } +} + + var iteratedTests = []struct { in, out string }{ @@ -68,6 +94,31 @@ func TestIterated(t *testing.T) { } } +var argonTestSalt = "12345678" +var argon2DeriveTests = []struct { + in, out string +}{ + {"hello", "bf69293d2961bbbebe4c64c745cf44d4"}, + {"world", "dc1bb06234b61c9542d8cf73e2e279d3"}, + {"foo", "7f6baa1c21f0e7eec16cf8fde866775d"}, + {"bar", "2826332c8e62d0cf97cc08f243c5cc9135654bf3a8e46d6a4b4637e42eda2fa0"}, + {"x", "89e5b79435132b98bbcad321532ae7e09f87ac96deca272d6012d367e6350b7d"}, + {"xxxxxxxxxxxxxxxxxxxxxxx", "de0f978013283457e29f0682e0078ad654e7c21bc72886c914c012e56fd5dc91"}, +} + +func TestArgon2Derive(t *testing.T) { + salt := []byte(argonTestSalt) + + for i, test := range argon2DeriveTests { + expected, _ := hex.DecodeString(test.out) + out := make([]byte, len(expected)) + Argon2(out, []byte(test.in), salt[:], 3, 4, 16) + if !bytes.Equal(expected, out) { + t.Errorf("#%d, got: %x want: %x", i, out, expected) + } + } +} + var parseTests = []struct { spec, in, out string dummyKey bool @@ -75,16 +126,19 @@ var parseTests = []struct { }{ /* Simple with SHA1 */ {"0002", "hello", "aaf4c61d", false, - Params{0, 0x02, nil, 0}}, + Params{SimpleS2K, 0x02, [16]byte{}, 0, 0, 0, 0}}, /* Salted with SHA1 */ {"01020102030405060708", "hello", "f4f7d67e", false, - Params{1, 0x02, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0}}, + Params{SaltedS2K, 0x02, [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0, 0, 0, 0}}, /* Iterated with SHA1 */ {"03020102030405060708f1", "hello", "f2a57b7c", false, - Params{3, 0x02, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0xf1}}, + Params{IteratedSaltedS2K, 0x02, [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0xf1, 0, 0, 0}}, + /* Argon2 */ + {"0401020304050607080102030405060708030410", "hello", "dabc018a", false, + Params{Argon2S2K, 0x00, [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0, 0x03, 0x04, 0x10}}, /* GNU dummy S2K */ {"6502474e5501", "", "", true, - Params{101, 0x02, nil, 0}}, + Params{GnuS2K, 0x02, [16]byte{}, 0, 0, 0, 0}}, } func TestParseIntoParams(t *testing.T) { @@ -98,8 +152,8 @@ func TestParseIntoParams(t *testing.T) { } if test.params.mode != params.mode || test.params.hashId != params.hashId || test.params.countByte != params.countByte || - !bytes.Equal(test.params.salt, params.salt) { - t.Errorf("%d: Wrong s2kconfig, got: %+v want: %+v", i, params, test.params) + !bytes.Equal(test.params.salt(), params.salt()) { + t.Errorf("%d: Wrong config, got: %+v want: %+v", i, params, test.params) } if params.Dummy() != test.dummyKey { @@ -136,46 +190,90 @@ func TestParseIntoParams(t *testing.T) { } } -func TestSerializeOK(t *testing.T) { +func TestSerializeSaltedOK(t *testing.T) { + hashes := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512, crypto.SHA224, crypto.SHA3_256, + crypto.SHA3_512} + for _, h := range hashes { + params := testSerializeConfigOK(t, &Config{S2KMode: SaltedS2K, Hash: h, PassphraseIsHighEntropy: true}) + + if params.mode != SaltedS2K { + t.Fatalf("Wrong mode, expected %d got %d", SaltedS2K, params.mode) + } + } +} + +func TestSerializeSaltedLowEntropy(t *testing.T) { + hashes := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512, crypto.SHA224, crypto.SHA3_256, + crypto.SHA3_512} + for _, h := range hashes { + params := testSerializeConfigOK(t, &Config{S2KMode: SaltedS2K, Hash: h}) + + if params.mode != IteratedSaltedS2K { + t.Fatalf("Wrong mode, expected %d got %d", IteratedSaltedS2K, params.mode) + } + + if params.countByte != 224 { // The default case. Corresponding to 16777216 + t.Fatalf("Wrong count byte, expected %d got %d", 224, params.countByte) + } + } +} + +func TestSerializeSaltedIteratedOK(t *testing.T) { hashes := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512, crypto.SHA224, crypto.SHA3_256, - crypto.SHA3_512, crypto.SHA1, crypto.RIPEMD160} - testCounts := []int{-1, 0, 1024, 65536, 4063232, 65011712} + crypto.SHA3_512} + // {input, expected} + testCounts := [][]int{{-1, 96}, {0, 224}, {1024, 96}, {65536, 96}, {4063232, 191}, {65011712, 255}} for _, h := range hashes { for _, c := range testCounts { - testSerializeConfigOK(t, &Config{Hash: h, S2KCount: c}) + params := testSerializeConfigOK(t, &Config{Hash: h, S2KCount: c[0]}) + + if params.mode != IteratedSaltedS2K { + t.Fatalf("Wrong mode, expected %d got %d", IteratedSaltedS2K, params.mode) + } + + if int(params.countByte) != c[1] { + t.Fatalf("Wrong count byte, expected %d got %d", c[1], params.countByte) + } } } } -func testSerializeConfigOK(t *testing.T, c *Config) { +func TestSerializeOKArgon(t *testing.T) { + config := &Config{ + S2KMode: Argon2S2K, + Argon2Config: &Argon2Config{NumberOfPasses: 3, DegreeOfParallelism: 4, Memory: 64*1024}, + } + + params := testSerializeConfigOK(t, config) + + if params.mode != Argon2S2K { + t.Fatalf("Wrong mode, expected %d got %d", Argon2S2K, params.mode) + } +} + +func testSerializeConfigOK(t *testing.T, c *Config) *Params { buf := bytes.NewBuffer(nil) key := make([]byte, 16) passphrase := []byte("testing") err := Serialize(buf, key, rand.Reader, passphrase, c) if err != nil { - t.Errorf("failed to serialize with config %+v: %s", c, err) - return + t.Fatalf("failed to serialize with config %+v: %s", c, err) } - f, err := Parse(buf) + f, err := Parse(bytes.NewBuffer(buf.Bytes())) if err != nil { - t.Errorf("failed to reparse: %s", err) - return + t.Fatalf("failed to reparse: %s", err) } key2 := make([]byte, len(key)) f(key2, passphrase) if !bytes.Equal(key2, key) { t.Errorf("keys don't match: %x (serialied) vs %x (parsed)", key, key2) } -} -func testSerializeConfigErr(t *testing.T, c *Config) { - buf := bytes.NewBuffer(nil) - key := make([]byte, 16) - passphrase := []byte("testing") - err := Serialize(buf, key, rand.Reader, passphrase, c) - if err == nil { - t.Errorf("expected to fail serialization with config %+v", c) - return + params, err := ParseIntoParams(bytes.NewBuffer(buf.Bytes())) + if err != nil { + t.Fatalf("failed to parse params: %s", err) } + + return params } diff --git a/openpgp/test_data/aead-sym-message.asc b/openpgp/test_data/aead-sym-message.asc deleted file mode 100644 index d9967a69..00000000 --- a/openpgp/test_data/aead-sym-message.asc +++ /dev/null @@ -1,369 +0,0 @@ ------BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.6.2 -Comment: https://openpgpjs.org - -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 -TvQZExKZfrZt -=KQtL ------END PGP MESSAGE----- diff --git a/openpgp/test_data/argon2-sym-message.asc b/openpgp/test_data/argon2-sym-message.asc new file mode 100644 index 00000000..5522075c --- /dev/null +++ b/openpgp/test_data/argon2-sym-message.asc @@ -0,0 +1,8 @@ +-----BEGIN PGP MESSAGE----- +Comment: Encrypted using AES with 128-bit key +Comment: Session key: 01FE16BBACFD1E7B78EF3B865187374F + +wycEBwScUvg8J/leUNU1RA7N/zE2AQQVnlL8rSLPP5VlQsunlO+ECxHSPgGYGKY+ +YJz4u6F+DDlDBOr5NRQXt/KJIf4m4mOlKyC/uqLbpnLJZMnTq3o79GxBTdIdOzhH +XfA3pqV4mTzF +-----END PGP MESSAGE----- \ No newline at end of file diff --git a/openpgp/write.go b/openpgp/write.go index 6e67473f..7fdd13a3 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -13,8 +13,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/ProtonMail/go-crypto/openpgp/s2k" ) // DetachSign signs message with the private key from signer (which must @@ -70,15 +70,11 @@ func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.S if signingKey.PrivateKey.Encrypted { return errors.InvalidArgumentError("signing key is encrypted") } + if _, ok := algorithm.HashToHashId(config.Hash()); !ok { + return errors.InvalidArgumentError("invalid hash function") + } - sig := new(packet.Signature) - sig.SigType = sigType - sig.PubKeyAlgo = signingKey.PrivateKey.PubKeyAlgo - sig.Hash = config.Hash() - sig.CreationTime = config.Now() - sigLifetimeSecs := config.SigLifetime() - sig.SigLifetimeSecs = &sigLifetimeSecs - sig.IssuerKeyId = &signingKey.PrivateKey.KeyId + sig := createSignaturePacket(signingKey.PublicKey, sigType, config) h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType) if err != nil { @@ -125,16 +121,13 @@ func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHi } var w io.WriteCloser - if config.AEAD() != nil { - w, err = packet.SerializeAEADEncrypted(ciphertext, key, config.Cipher(), config.AEAD().Mode(), config) - if err != nil { - return - } - } else { - w, err = packet.SerializeSymmetricallyEncrypted(ciphertext, config.Cipher(), key, config) - if err != nil { - return - } + cipherSuite := packet.CipherSuite{ + Cipher: config.Cipher(), + Mode: config.AEAD().Mode(), + } + w, err = packet.SerializeSymmetricallyEncrypted(ciphertext, config.Cipher(), config.AEAD() != nil, cipherSuite, key, config) + if err != nil { + return } literalData := w @@ -173,8 +166,25 @@ func intersectPreferences(a []uint8, b []uint8) (intersection []uint8) { return a[:j] } +// intersectPreferences mutates and returns a prefix of a that contains only +// the values in the intersection of a and b. The order of a is preserved. +func intersectCipherSuites(a [][2]uint8, b [][2]uint8) (intersection [][2]uint8) { + var j int + for _, v := range a { + for _, v2 := range b { + if v[0] == v2[0] && v[1] == v2[1] { + a[j] = v + j++ + break + } + } + } + + return a[:j] +} + func hashToHashId(h crypto.Hash) uint8 { - v, ok := s2k.HashToHashId(h) + v, ok := algorithm.HashToHashId(h) if !ok { panic("tried to convert unknown hash") } @@ -240,7 +250,7 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit var hash crypto.Hash for _, hashId := range candidateHashes { - if h, ok := s2k.HashIdToHash(hashId); ok && h.Available() { + if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() { hash = h break } @@ -249,7 +259,7 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit // If the hash specified by config is a candidate, we'll use that. if configuredHash := config.Hash(); configuredHash.Available() { for _, hashId := range candidateHashes { - if h, ok := s2k.HashIdToHash(hashId); ok && h == configuredHash { + if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash { hash = h break } @@ -258,7 +268,7 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit if hash == 0 { hashId := candidateHashes[0] - name, ok := s2k.HashIdToString(hashId) + name, ok := algorithm.HashIdToString(hashId) if !ok { name = "#" + strconv.Itoa(int(hashId)) } @@ -329,10 +339,10 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En // These are the possible ciphers that we'll use for the message. candidateCiphers := []uint8{ - uint8(packet.CipherAES128), uint8(packet.CipherAES256), - uint8(packet.CipherCAST5), + uint8(packet.CipherAES128), } + // These are the possible hash functions that we'll use for the signature. candidateHashes := []uint8{ hashToHashId(crypto.SHA256), @@ -340,14 +350,18 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En hashToHashId(crypto.SHA512), hashToHashId(crypto.SHA3_256), hashToHashId(crypto.SHA3_512), - hashToHashId(crypto.SHA1), - hashToHashId(crypto.RIPEMD160), } - candidateAeadModes := []uint8{ - uint8(packet.AEADModeEAX), - uint8(packet.AEADModeOCB), - uint8(packet.AEADModeExperimentalGCM), + + // Prefer GCM if everyone supports it + candidateCipherSuites := [][2]uint8{ + {uint8(packet.CipherAES256), uint8(packet.AEADModeGCM)}, + {uint8(packet.CipherAES256), uint8(packet.AEADModeEAX)}, + {uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeGCM)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeEAX)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}, } + candidateCompression := []uint8{ uint8(packet.CompressionNone), uint8(packet.CompressionZIP), @@ -355,8 +369,9 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En } encryptKeys := make([]Key, len(to)) - // AEAD is used only if every key supports it. - aeadSupported := true + + // AEAD is used only if config enables it and every key supports it + aeadSupported := config.AEAD() != nil for i := range to { var ok bool @@ -366,13 +381,13 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En } sig := to[i].PrimaryIdentity().SelfSignature - if sig.AEAD == false { + if !sig.SEIPDv2 { aeadSupported = false } candidateCiphers = intersectPreferences(candidateCiphers, sig.PreferredSymmetric) candidateHashes = intersectPreferences(candidateHashes, sig.PreferredHash) - candidateAeadModes = intersectPreferences(candidateAeadModes, sig.PreferredAEAD) + candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, sig.PreferredCipherSuites) candidateCompression = intersectPreferences(candidateCompression, sig.PreferredCompression) } @@ -386,13 +401,17 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#hash-algos candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } - if len(candidateAeadModes) == 0 { + if len(candidateCipherSuites) == 0 { // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateAeadModes = []uint8{uint8(packet.AEADModeEAX)} + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} } cipher := packet.CipherFunction(candidateCiphers[0]) - mode := packet.AEADMode(candidateAeadModes[0]) + aeadCipherSuite := packet.CipherSuite{ + Cipher: packet.CipherFunction(candidateCipherSuites[0][0]), + Mode: packet.AEADMode(candidateCipherSuites[0][1]), + } + // If the cipher specified by config is a candidate, we'll use that. configuredCipher := config.Cipher() for _, c := range candidateCiphers { @@ -415,17 +434,11 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En } var payload io.WriteCloser - if config.AEAD() != nil && aeadSupported { - payload, err = packet.SerializeAEADEncrypted(dataWriter, symKey, cipher, mode, config) - if err != nil { - return - } - } else { - payload, err = packet.SerializeSymmetricallyEncrypted(dataWriter, cipher, symKey, config) - if err != nil { - return - } + payload, err = packet.SerializeSymmetricallyEncrypted(dataWriter, cipher, aeadSupported, aeadCipherSuite, symKey, config) + if err != nil { + return } + payload, err = handleCompression(payload, candidateCompression, config) if err != nil { return nil, err @@ -450,8 +463,6 @@ func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Con hashToHashId(crypto.SHA512), hashToHashId(crypto.SHA3_256), hashToHashId(crypto.SHA3_512), - hashToHashId(crypto.SHA1), - hashToHashId(crypto.RIPEMD160), } defaultHashes := candidateHashes[0:1] preferredHashes := signed.PrimaryIdentity().SelfSignature.PreferredHash @@ -494,15 +505,9 @@ func (s signatureWriter) Write(data []byte) (int, error) { } func (s signatureWriter) Close() error { - sig := &packet.Signature{ - Version: s.signer.Version, - SigType: s.sigType, - PubKeyAlgo: s.signer.PubKeyAlgo, - Hash: s.hashType, - CreationTime: s.config.Now(), - IssuerKeyId: &s.signer.KeyId, - Metadata: s.metadata, - } + sig := createSignaturePacket(&s.signer.PublicKey, s.sigType, s.config) + sig.Hash = s.hashType + sig.Metadata = s.metadata if err := sig.Sign(s.h, s.signer, s.config); err != nil { return err @@ -516,6 +521,21 @@ func (s signatureWriter) Close() error { return s.encryptedData.Close() } +func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature { + sigLifetimeSecs := config.SigLifetime() + return &packet.Signature{ + Version: signer.Version, + SigType: sigType, + PubKeyAlgo: signer.PubKeyAlgo, + Hash: config.Hash(), + CreationTime: config.Now(), + IssuerKeyId: &signer.KeyId, + IssuerFingerprint: signer.Fingerprint, + Notations: config.Notations(), + SigLifetimeSecs: &sigLifetimeSecs, + } +} + // noOpCloser is like an ioutil.NopCloser, but for an io.Writer. // TODO: we have two of these in OpenPGP packages alone. This probably needs // to be promoted somewhere more common. diff --git a/openpgp/write_test.go b/openpgp/write_test.go index a50aadfa..28f5a96d 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -15,6 +15,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" ) const ( @@ -72,6 +73,111 @@ func TestSignDetachedP256(t *testing.T) { testDetachedSignature(t, kring, out, signedInput, "check", testKeyP256KeyId) } +func TestSignDetachedWithNotation(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + signature := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + config := &packet.Config{ + SignatureNotations: []*packet.Notation{ + { + Name: "test@example.com", + Value: []byte("test"), + IsHumanReadable: true, + }, + }, + } + err := DetachSign(signature, kring[0], message, config) + if err != nil { + t.Error(err) + } + + signed := bytes.NewBufferString(signedInput) + config = &packet.Config{} + sig, signer, err := VerifyDetachedSignature(kring, signed, signature, config) + if err != nil { + t.Errorf("signature error: %s", err) + return + } + if sig == nil { + t.Errorf("sig is nil") + return + } + if numNotations, numExpected := len(sig.Notations), 1; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + if sig.Notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + if sig.Notations[0].Name != "test@example.com" { + t.Fatalf("got %s, expected test@example.com", sig.Notations[0].Name) + } + if string(sig.Notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(sig.Notations[0].Value)) + } + if signer == nil { + t.Errorf("signer is nil") + return + } + if signer.PrimaryKey.KeyId != testKey1KeyId { + t.Errorf("wrong signer: got %x, expected %x", signer.PrimaryKey.KeyId, testKey1KeyId) + } +} + +func TestSignDetachedWithCriticalNotation(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + signature := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + config := &packet.Config{ + SignatureNotations: []*packet.Notation{ + { + Name: "test@example.com", + Value: []byte("test"), + IsHumanReadable: true, + IsCritical: true, + }, + }, + } + err := DetachSign(signature, kring[0], message, config) + if err != nil { + t.Error(err) + } + + signed := bytes.NewBufferString(signedInput) + config = &packet.Config{ + KnownNotations: map[string]bool{ + "test@example.com": true, + }, + } + sig, signer, err := VerifyDetachedSignature(kring, signed, signature, config) + if err != nil { + t.Errorf("signature error: %s", err) + return + } + if sig == nil { + t.Errorf("sig is nil") + return + } + if numNotations, numExpected := len(sig.Notations), 1; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + if sig.Notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + if sig.Notations[0].Name != "test@example.com" { + t.Fatalf("got %s, expected test@example.com", sig.Notations[0].Name) + } + if string(sig.Notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(sig.Notations[0].Value)) + } + if signer == nil { + t.Errorf("signer is nil") + return + } + if signer.PrimaryKey.KeyId != testKey1KeyId { + t.Errorf("wrong signer: got %x, expected %x", signer.PrimaryKey.KeyId, testKey1KeyId) + } +} + func TestNewEntity(t *testing.T) { // Check bit-length with no config. @@ -201,48 +307,58 @@ func TestEncryptWithCompression(t *testing.T) { } func TestSymmetricEncryption(t *testing.T) { - buf := new(bytes.Buffer) - plaintext, err := SymmetricallyEncrypt(buf, []byte("testing"), nil, nil) - if err != nil { - t.Errorf("error writing headers: %s", err) - return - } - message := []byte("hello world\n") - _, err = plaintext.Write(message) - if err != nil { - t.Errorf("error writing to plaintext writer: %s", err) - } - err = plaintext.Close() - if err != nil { - t.Errorf("error closing plaintext writer: %s", err) - } + modesS2K := map[string]s2k.Mode{ + "Iterated": s2k.IteratedSaltedS2K, + "Argon2": s2k.Argon2S2K, + } + for s2kName, s2ktype := range modesS2K { + t.Run(s2kName, func(t *testing.T) { + config := &packet.Config{ + S2KConfig: &s2k.Config{S2KMode: s2ktype}, + } + buf := new(bytes.Buffer) + plaintext, err := SymmetricallyEncrypt(buf, []byte("testing"), nil, config) + if err != nil { + t.Errorf("error writing headers: %s", err) + return + } + message := []byte("hello world\n") + _, err = plaintext.Write(message) + if err != nil { + t.Errorf("error writing to plaintext writer: %s", err) + } + err = plaintext.Close() + if err != nil { + t.Errorf("error closing plaintext writer: %s", err) + } - md, err := ReadMessage(buf, nil, func(keys []Key, symmetric bool) ([]byte, error) { - return []byte("testing"), nil - }, nil) - if err != nil { - t.Errorf("error rereading message: %s", err) - } - messageBuf := bytes.NewBuffer(nil) - _, err = io.Copy(messageBuf, md.UnverifiedBody) - if err != nil { - t.Errorf("error rereading message: %s", err) - } - if !bytes.Equal(message, messageBuf.Bytes()) { - t.Errorf("recovered message incorrect got '%s', want '%s'", messageBuf.Bytes(), message) + md, err := ReadMessage(buf, nil, func(keys []Key, symmetric bool) ([]byte, error) { + return []byte("testing"), nil + }, nil) + if err != nil { + t.Errorf("error rereading message: %s", err) + } + messageBuf := bytes.NewBuffer(nil) + _, err = io.Copy(messageBuf, md.UnverifiedBody) + if err != nil { + t.Errorf("error rereading message: %s", err) + } + if !bytes.Equal(message, messageBuf.Bytes()) { + t.Errorf("recovered message incorrect got '%s', want '%s'", messageBuf.Bytes(), message) + } + }) } } func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { - var modes = []packet.AEADMode{ - packet.AEADModeEAX, - packet.AEADModeOCB, - packet.AEADModeExperimentalGCM, + modesS2K := map[int]s2k.Mode{ + 0: s2k.IteratedSaltedS2K, + 1: s2k.Argon2S2K, } aeadConf := packet.AEADConfig{ - DefaultMode: modes[mathrand.Intn(len(modes))], + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], } - config := &packet.Config{AEADConfig: &aeadConf} + config := &packet.Config{AEADConfig: &aeadConf, S2KConfig: &s2k.Config{S2KMode: modesS2K[mathrand.Intn(2)]}} buf := new(bytes.Buffer) passphrase := make([]byte, mathrand.Intn(maxPassLen)) _, err := rand.Read(passphrase) @@ -280,12 +396,15 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { default: t.Errorf("Didn't find a SymmetricKeyEncrypted packet (found %T instead)", tp) } - // Then an AEADEncrypted packet + // Then an SymmetricallyEncrypted packet version 2 p, err = packets.Next() switch tp := p.(type) { - case *packet.AEADEncrypted: + case *packet.SymmetricallyEncrypted: + if tp.Version != 2 { + t.Errorf("Wrong packet version, expected 2, found %d", tp.Version) + } default: - t.Errorf("Didn't find an AEADEncrypted packet (found %T instead)", tp) + t.Errorf("Didn't find an SymmetricallyEncrypted packet (found %T instead)", tp) } promptFunc := func(keys []Key, symmetric bool) ([]byte, error) { @@ -369,16 +488,11 @@ func TestEncryption(t *testing.T) { DefaultCompressionAlgo: compAlgo, CompressionConfig: compConf, } - // Flip coin to enable AEAD mode - var modes = []packet.AEADMode{ - packet.AEADModeEAX, - packet.AEADModeOCB, - packet.AEADModeExperimentalGCM, - } + // Flip coin to enable AEAD mode if mathrand.Int()%2 == 0 { aeadConf := packet.AEADConfig{ - DefaultMode: modes[mathrand.Intn(len(modes))], + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], } config.AEADConfig = &aeadConf }