From eaa4e03cfed731b5a1c2756add1eb28d34af05d6 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sat, 18 Jun 2022 13:47:00 +0200 Subject: [PATCH] tests: finish parsing and X25519 tests, distinguish HMAC errors If the implementation re-encodes the header before checking the HMAC, that would mask malleability issues: the HMAC check would fail because the tests HMAC'd the original header, but an attacker could also produce the right HMAC. Instead of duplicating every parsing tests (with the original and re-encoded HMAC), we make the test framework distinguish HMAC errors, which ensures bad encodings are recognized as such and not bypassable HMAC errors. --- internal/format/format.go | 2 +- internal/testkit/testkit.go | 38 +++++++++++++----- testdata/testkit/{crlf => header_crlf} | 0 testdata/testkit/hmac_bad | 2 +- ...g_file_key_scrypt => scrypt_long_file_key} | Bin testdata/testkit/stanza_bad_start | 11 +++++ testdata/testkit/stanza_base64_padding | 12 ++++++ testdata/testkit/stanza_empty_argument | 11 +++++ testdata/testkit/stanza_empty_last_line | 3 +- testdata/testkit/stanza_invalid_character | 11 +++++ testdata/testkit/stanza_long_line | 13 ++++++ testdata/testkit/stanza_no_arguments | 11 +++++ testdata/testkit/stanza_not_canonical | 12 ++++++ testdata/testkit/stanza_spurious_cr | 11 +++++ testdata/testkit/version_unsupported | 9 +++++ testdata/testkit/x25519_bad_tag | 10 +++++ testdata/testkit/x25519_grease | 14 +++++++ .../{long_file_key => x25519_long_file_key} | 0 testdata/testkit/x25519_long_share | 10 +++++ testdata/testkit/x25519_lowercase | 10 +++++ testdata/testkit/x25519_short_share | 10 +++++ testkit_test.go | 35 +++++++++------- tests/{crlf.go => header_crlf.go} | 0 tests/hmac_bad.go | 2 +- ..._key_scrypt.go => scrypt_long_file_key.go} | 0 tests/stanza_bad_start.go | 21 ++++++++++ tests/stanza_base64_padding.go | 26 ++++++++++++ tests/stanza_empty_argument.go | 21 ++++++++++ tests/stanza_empty_last_line.go | 2 +- tests/stanza_invalid_character.go | 21 ++++++++++ tests/stanza_long_line.go | 27 +++++++++++++ tests/stanza_no_arguments.go | 21 ++++++++++ tests/stanza_not_canonical.go | 26 ++++++++++++ tests/stanza_spurious_cr.go | 25 ++++++++++++ tests/version_unsupported.go | 19 +++++++++ tests/x25519_bad_tag.go | 27 +++++++++++++ tests/x25519_grease.go | 22 ++++++++++ tests/x25519_identity.go | 18 ++------- ...ng_file_key.go => x25519_long_file_key.go} | 0 tests/x25519_long_share.go | 30 ++++++++++++++ tests/x25519_low_order.go | 16 +------- tests/x25519_lowercase.go | 27 +++++++++++++ tests/x25519_short_share.go | 30 ++++++++++++++ 43 files changed, 557 insertions(+), 59 deletions(-) rename testdata/testkit/{crlf => header_crlf} (100%) rename testdata/testkit/{long_file_key_scrypt => scrypt_long_file_key} (100%) create mode 100644 testdata/testkit/stanza_bad_start create mode 100644 testdata/testkit/stanza_base64_padding create mode 100644 testdata/testkit/stanza_empty_argument create mode 100644 testdata/testkit/stanza_invalid_character create mode 100644 testdata/testkit/stanza_long_line create mode 100644 testdata/testkit/stanza_no_arguments create mode 100644 testdata/testkit/stanza_not_canonical create mode 100644 testdata/testkit/stanza_spurious_cr create mode 100644 testdata/testkit/version_unsupported create mode 100644 testdata/testkit/x25519_bad_tag create mode 100644 testdata/testkit/x25519_grease rename testdata/testkit/{long_file_key => x25519_long_file_key} (100%) create mode 100644 testdata/testkit/x25519_long_share create mode 100644 testdata/testkit/x25519_lowercase create mode 100644 testdata/testkit/x25519_short_share rename tests/{crlf.go => header_crlf.go} (100%) rename tests/{long_file_key_scrypt.go => scrypt_long_file_key.go} (100%) create mode 100644 tests/stanza_bad_start.go create mode 100644 tests/stanza_base64_padding.go create mode 100644 tests/stanza_empty_argument.go create mode 100644 tests/stanza_invalid_character.go create mode 100644 tests/stanza_long_line.go create mode 100644 tests/stanza_no_arguments.go create mode 100644 tests/stanza_not_canonical.go create mode 100644 tests/stanza_spurious_cr.go create mode 100644 tests/version_unsupported.go create mode 100644 tests/x25519_bad_tag.go create mode 100644 tests/x25519_grease.go rename tests/{long_file_key.go => x25519_long_file_key.go} (100%) create mode 100644 tests/x25519_long_share.go create mode 100644 tests/x25519_lowercase.go create mode 100644 tests/x25519_short_share.go diff --git a/internal/format/format.go b/internal/format/format.go index 94925d9..a91064a 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -258,7 +258,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) { return nil, nil, errorf("malformed closing line: %q", line) } h.MAC, err = DecodeString(args[0]) - if err != nil { + if err != nil || len(h.MAC) != 32 { return nil, nil, errorf("malformed closing line %q: %v", line, err) } break diff --git a/internal/testkit/testkit.go b/internal/testkit/testkit.go index b2410ae..60e0823 100644 --- a/internal/testkit/testkit.go +++ b/internal/testkit/testkit.go @@ -106,16 +106,22 @@ func (f *TestFile) Body(body []byte) { } } -func (f *TestFile) Stanza(args []string, body []byte) { - f.ArgsLine(args...) - f.Body(body) -} - func (f *TestFile) AEADBody(key, body []byte) { aead, _ := chacha20poly1305.New(key) f.Body(aead.Seal(nil, make([]byte, chacha20poly1305.NonceSize), body, nil)) } +func x25519(scalar, point []byte) []byte { + secret, err := curve25519.X25519(scalar, point) + if err != nil { + if err.Error() == "bad input point: low order point" { + return make([]byte, 32) + } + panic(err) + } + return secret +} + func (f *TestFile) X25519(identity []byte) { f.X25519RecordIdentity(identity) f.X25519NoRecordIdentity(identity) @@ -127,11 +133,16 @@ func (f *TestFile) X25519RecordIdentity(identity []byte) { } func (f *TestFile) X25519NoRecordIdentity(identity []byte) { - recipient, _ := curve25519.X25519(identity, curve25519.Basepoint) - ephemeral := f.Rand(32) - share, _ := curve25519.X25519(ephemeral, curve25519.Basepoint) + share := x25519(f.Rand(32), curve25519.Basepoint) + f.X25519Stanza(share, identity) +} + +func (f *TestFile) X25519Stanza(share, identity []byte) { + recipient := x25519(identity, curve25519.Basepoint) f.ArgsLine("X25519", b64(share)) - secret, _ := curve25519.X25519(ephemeral, recipient) + // This would be ordinarily done as [ephemeral]recipient rather than + // [identity]share, but for some tests we don't have the dlog of share. + secret := x25519(identity, share) key := make([]byte, 32) hkdf.New(sha256.New, secret, append(share, recipient...), []byte("age-encryption.org/v1/X25519")).Read(key) @@ -150,8 +161,11 @@ func (f *TestFile) ScryptRecordPassphrase(passphrase string) { func (f *TestFile) ScryptNoRecordPassphrase(passphrase string, workFactor int) { salt := f.Rand(16) f.ArgsLine("scrypt", b64(salt), strconv.Itoa(workFactor)) - key, _ := scrypt.Key([]byte(passphrase), append([]byte("age-encryption.org/v1/scrypt"), salt...), + key, err := scrypt.Key([]byte(passphrase), append([]byte("age-encryption.org/v1/scrypt"), salt...), 1< X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-- stanza + +--- lpxzkyQGe/sA7F1yh4c6KVZV7//jANm5lYefTToioXs +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_base64_padding b/testdata/testkit/stanza_base64_padding new file mode 100644 index 0000000..18ad2d2 --- /dev/null +++ b/testdata/testkit/stanza_base64_padding @@ -0,0 +1,12 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> stanza +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUE= +--- OtG7IuNHaf2SHZuowmxg/fhbhtz0/DI5g5OGd7WH7S0 +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_empty_argument b/testdata/testkit/stanza_empty_argument new file mode 100644 index 0000000..cb9ef0b --- /dev/null +++ b/testdata/testkit/stanza_empty_argument @@ -0,0 +1,11 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> stanza argument + +--- bosBxVRBzKF9emyxQ9BERq7+D5JKU+lvbEsL8UHJ/SA +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_empty_last_line b/testdata/testkit/stanza_empty_last_line index 285f0d8..30a6c8f 100644 --- a/testdata/testkit/stanza_empty_last_line +++ b/testdata/testkit/stanza_empty_last_line @@ -8,6 +8,7 @@ age-encryption.org/v1 EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U -> stanza QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB ---- +3PC416gxa7Mk7WxpX0kb6DVfSuCun0niGre+G4bZhE +--- cb4SqtunSJzXKDGjqeYxuva9Be80QXEDKDn2aKBaCsw îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_invalid_character b/testdata/testkit/stanza_invalid_character new file mode 100644 index 0000000..d5c8b43 --- /dev/null +++ b/testdata/testkit/stanza_invalid_character @@ -0,0 +1,11 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> stanza è + +--- sTIB/0Fc74rhpjC4RAxoR3E01eVTTnWruaD+c5QWjKI +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_long_line b/testdata/testkit/stanza_long_line new file mode 100644 index 0000000..5435260 --- /dev/null +++ b/testdata/testkit/stanza_long_line @@ -0,0 +1,13 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 +comment: a body line is longer than 64 columns + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> stanza +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + +--- tnRUR2vmmU92czsjnioF5ujgXUetUhzUoQPPGT9wmug +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_no_arguments b/testdata/testkit/stanza_no_arguments new file mode 100644 index 0000000..a667ad9 --- /dev/null +++ b/testdata/testkit/stanza_no_arguments @@ -0,0 +1,11 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> + +--- B0qjnUjVajTa8I4Uia49g1c4DMQQN6u9m9QOSS1HLks +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_not_canonical b/testdata/testkit/stanza_not_canonical new file mode 100644 index 0000000..f7794aa --- /dev/null +++ b/testdata/testkit/stanza_not_canonical @@ -0,0 +1,12 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> stanza +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUF +--- nQM2VCzmNLPrUurNWN+SW9wVp/9uTMQ/6CTUM7l8c84 +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/stanza_spurious_cr b/testdata/testkit/stanza_spurious_cr new file mode 100644 index 0000000..b85c2ce --- /dev/null +++ b/testdata/testkit/stanza_spurious_cr @@ -0,0 +1,11 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> stanza +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +--- MZaFAh8ldzU0F88NJjLx5yd7fnd57XS5COowmgvQtXQ +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/version_unsupported b/testdata/testkit/version_unsupported new file mode 100644 index 0000000..e7a7ddf --- /dev/null +++ b/testdata/testkit/version_unsupported @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1234 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +--- 38AL8Mr4VwmS6CNbM4bc7u3WwGBDqsMTRHOuYJ9ckqs +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/x25519_bad_tag b/testdata/testkit/x25519_bad_tag new file mode 100644 index 0000000..9a08745 --- /dev/null +++ b/testdata/testkit/x25519_bad_tag @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 +comment: the ChaCha20Poly1305 authentication tag on the body of the X25519 stanza is wrong + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw0o +--- tG0k9bg4iIuBdMWb13n7FFYDzoBbtsLppNLhbh22aKg +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/x25519_grease b/testdata/testkit/x25519_grease new file mode 100644 index 0000000..aa212d9 --- /dev/null +++ b/testdata/testkit/x25519_grease @@ -0,0 +1,14 @@ +expect: success +payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 + +age-encryption.org/v1 +-> grease + +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +-> grease + +--- 7NLrfbRUZt6qK0pdtARUf59dHwo12ReldjJKjMlbE3I +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/long_file_key b/testdata/testkit/x25519_long_file_key similarity index 100% rename from testdata/testkit/long_file_key rename to testdata/testkit/x25519_long_file_key diff --git a/testdata/testkit/x25519_long_share b/testdata/testkit/x25519_long_share new file mode 100644 index 0000000..a2f3a04 --- /dev/null +++ b/testdata/testkit/x25519_long_share @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 +comment: a trailing zero is missing from the X25519 share + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCcA +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- QbEwdWirchS37UUOPh7uVddRiOaWjFwRUpaQ4Q+Z1RE +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/x25519_lowercase b/testdata/testkit/x25519_lowercase new file mode 100644 index 0000000..853d408 --- /dev/null +++ b/testdata/testkit/x25519_lowercase @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 +comment: the first argument in the X25519 stanza is lowercase + +age-encryption.org/v1 +-> x25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +--- SwXKO3dXLh9l5QiSgMWgPhCkwstT8oB4jLDv7aBgC+c +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/testdata/testkit/x25519_short_share b/testdata/testkit/x25519_short_share new file mode 100644 index 0000000..7feb27e --- /dev/null +++ b/testdata/testkit/x25519_short_share @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 +comment: a trailing zero is missing from the X25519 share + +age-encryption.org/v1 +-> X25519 l7o4oTX9X5E3/KODa/7CQ0CrA9fKMWsm9IJjYzSlJg +yUGP5aPob6YJ+vzRfBtDT9D1K/wmyheZE/Xl/mDSKA4 +--- Zn1/VRtHpD93HtIXSv1S++POXeKcQF7w1+hpXhMiAbk +¬]?7åPqÓ¦ F—¹ •Â÷õÛ®è zŒ(rŠóÎ| \ No newline at end of file diff --git a/testkit_test.go b/testkit_test.go index 578efcf..3a44b48 100644 --- a/testkit_test.go +++ b/testkit_test.go @@ -75,10 +75,9 @@ func TestVectors(t *testing.T) { func testVector(t *testing.T, test []byte) { var ( - expectHeaderFailure bool - expectPayloadFailure bool - payloadHash *[32]byte - identities []age.Identity + expect string + payloadHash *[32]byte + identities []age.Identity ) for { @@ -95,13 +94,13 @@ func testVector(t *testing.T, test []byte) { case "expect": switch value { case "success": + case "HMAC failure": case "header failure": - expectHeaderFailure = true case "payload failure": - expectPayloadFailure = true default: t.Fatal("invalid test file: unknown expect value:", value) } + expect = value case "payload": h, err := hex.DecodeString(value) if err != nil { @@ -130,24 +129,30 @@ func testVector(t *testing.T, test []byte) { } r, err := age.Decrypt(bytes.NewReader(test), identities...) - if err != nil { - if expectHeaderFailure { + if err != nil && strings.HasSuffix(err.Error(), "bad header MAC") { + if expect == "HMAC failure" { t.Log(err) return } - t.Fatal("unexpected header error:", err) - } else if expectHeaderFailure { - t.Fatal("expected header error") + t.Fatalf("expected %s, got HMAC error", expect) + } else if err != nil { + if expect == "header failure" { + t.Log(err) + return + } + t.Fatalf("expected %s, got: %v", expect, err) + } else if expect != "success" && expect != "payload failure" { + t.Fatalf("expected %s, got success", expect) } out, err := io.ReadAll(r) if err != nil { - if expectPayloadFailure { + if expect == "payload failure" { t.Log(err) return } - t.Fatal("unexpected payload error:", err) - } else if expectPayloadFailure { - t.Fatal("expected payload error") + t.Fatalf("expected %s, got: %v", expect, err) + } else if expect != "success" { + t.Fatalf("expected %s, got success", expect) } if sha256.Sum256(out) != *payloadHash { t.Error("payload hash mismatch") diff --git a/tests/crlf.go b/tests/header_crlf.go similarity index 100% rename from tests/crlf.go rename to tests/header_crlf.go diff --git a/tests/hmac_bad.go b/tests/hmac_bad.go index a8556f0..b6a7eba 100644 --- a/tests/hmac_bad.go +++ b/tests/hmac_bad.go @@ -16,6 +16,6 @@ func main() { f.HMAC() f.FileKey(testkit.TestFileKey) f.Payload("age") - f.ExpectHeaderFailure() + f.ExpectHMACFailure() f.Generate() } diff --git a/tests/long_file_key_scrypt.go b/tests/scrypt_long_file_key.go similarity index 100% rename from tests/long_file_key_scrypt.go rename to tests/scrypt_long_file_key.go diff --git a/tests/stanza_bad_start.go b/tests/stanza_bad_start.go new file mode 100644 index 0000000..3e473c2 --- /dev/null +++ b/tests/stanza_bad_start.go @@ -0,0 +1,21 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import "filippo.io/age/internal/testkit" + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.TextLine("-- stanza") + f.Body([]byte("")) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/stanza_base64_padding.go b/tests/stanza_base64_padding.go new file mode 100644 index 0000000..0c81221 --- /dev/null +++ b/tests/stanza_base64_padding.go @@ -0,0 +1,26 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "bytes" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("stanza") + f.Body(bytes.Repeat([]byte("A"), 50)) + f.TextLine(f.UnreadLine() + "=") + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/stanza_empty_argument.go b/tests/stanza_empty_argument.go new file mode 100644 index 0000000..60b707f --- /dev/null +++ b/tests/stanza_empty_argument.go @@ -0,0 +1,21 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import "filippo.io/age/internal/testkit" + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("stanza", "", "argument") + f.Body([]byte("")) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/stanza_empty_last_line.go b/tests/stanza_empty_last_line.go index 519be1e..8ed8f78 100644 --- a/tests/stanza_empty_last_line.go +++ b/tests/stanza_empty_last_line.go @@ -17,7 +17,7 @@ func main() { f.VersionLine("v1") f.X25519(testkit.TestX25519Recipient) f.ArgsLine("stanza") - f.Body(bytes.Repeat([]byte("A"), 48)) + f.Body(bytes.Repeat([]byte("A"), 48*2)) f.HMAC() f.Payload("age") f.Generate() diff --git a/tests/stanza_invalid_character.go b/tests/stanza_invalid_character.go new file mode 100644 index 0000000..bd9c7e3 --- /dev/null +++ b/tests/stanza_invalid_character.go @@ -0,0 +1,21 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import "filippo.io/age/internal/testkit" + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("stanza", "è") + f.Body([]byte("")) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/stanza_long_line.go b/tests/stanza_long_line.go new file mode 100644 index 0000000..5eb7d5c --- /dev/null +++ b/tests/stanza_long_line.go @@ -0,0 +1,27 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "strings" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("stanza") + f.TextLine(strings.Repeat("A", 68)) + f.TextLine("") + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("a body line is longer than 64 columns") + f.Generate() +} diff --git a/tests/stanza_no_arguments.go b/tests/stanza_no_arguments.go new file mode 100644 index 0000000..ecba530 --- /dev/null +++ b/tests/stanza_no_arguments.go @@ -0,0 +1,21 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import "filippo.io/age/internal/testkit" + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine() + f.Body([]byte("")) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/stanza_not_canonical.go b/tests/stanza_not_canonical.go new file mode 100644 index 0000000..88935e7 --- /dev/null +++ b/tests/stanza_not_canonical.go @@ -0,0 +1,26 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "bytes" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("stanza") + f.Body(bytes.Repeat([]byte("A"), 50)) + f.TextLine(testkit.NotCanonicalBase64(f.UnreadLine())) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/stanza_spurious_cr.go b/tests/stanza_spurious_cr.go new file mode 100644 index 0000000..b898616 --- /dev/null +++ b/tests/stanza_spurious_cr.go @@ -0,0 +1,25 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "strings" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("stanza") + f.TextLine(strings.Repeat("A", 32) + "\r" + strings.Repeat("A", 31)) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/version_unsupported.go b/tests/version_unsupported.go new file mode 100644 index 0000000..e1fc997 --- /dev/null +++ b/tests/version_unsupported.go @@ -0,0 +1,19 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import "filippo.io/age/internal/testkit" + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1234") + f.X25519(testkit.TestX25519Recipient) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/x25519_bad_tag.go b/tests/x25519_bad_tag.go new file mode 100644 index 0000000..e56cd7c --- /dev/null +++ b/tests/x25519_bad_tag.go @@ -0,0 +1,27 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "encoding/base64" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + body, _ := base64.RawStdEncoding.DecodeString(f.UnreadLine()) + body[len(body)-1] ^= 0xff + f.TextLine(base64.RawStdEncoding.EncodeToString(body)) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("the ChaCha20Poly1305 authentication tag on the body of the X25519 stanza is wrong") + f.Generate() +} diff --git a/tests/x25519_grease.go b/tests/x25519_grease.go new file mode 100644 index 0000000..98f7148 --- /dev/null +++ b/tests/x25519_grease.go @@ -0,0 +1,22 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import "filippo.io/age/internal/testkit" + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.ArgsLine("grease") + f.Body(nil) + f.X25519(testkit.TestX25519Recipient) + f.ArgsLine("grease") + f.Body(nil) + f.HMAC() + f.Payload("age") + f.Generate() +} diff --git a/tests/x25519_identity.go b/tests/x25519_identity.go index be8e531..f1b86e6 100644 --- a/tests/x25519_identity.go +++ b/tests/x25519_identity.go @@ -6,26 +6,14 @@ package main -import ( - "crypto/sha256" - "encoding/base64" - - "filippo.io/age/internal/testkit" - "golang.org/x/crypto/curve25519" - "golang.org/x/crypto/hkdf" -) +import "filippo.io/age/internal/testkit" func main() { f := testkit.NewTestFile() f.VersionLine("v1") f.X25519RecordIdentity(testkit.TestX25519Identity) - share := make([]byte, curve25519.PointSize) - f.ArgsLine("X25519", base64.RawStdEncoding.EncodeToString(share)) - secret := make([]byte, curve25519.PointSize) - key := make([]byte, 32) - hkdf.New(sha256.New, secret, append(share, testkit.TestX25519Recipient...), - []byte("age-encryption.org/v1/X25519")).Read(key) - f.AEADBody(key, testkit.TestFileKey) + share := make([]byte, 32) + f.X25519Stanza(share, testkit.TestX25519Identity) f.HMAC() f.Payload("age") f.ExpectHeaderFailure() diff --git a/tests/long_file_key.go b/tests/x25519_long_file_key.go similarity index 100% rename from tests/long_file_key.go rename to tests/x25519_long_file_key.go diff --git a/tests/x25519_long_share.go b/tests/x25519_long_share.go new file mode 100644 index 0000000..eca52d4 --- /dev/null +++ b/tests/x25519_long_share.go @@ -0,0 +1,30 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "encoding/base64" + + "filippo.io/age/internal/testkit" + "golang.org/x/crypto/curve25519" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + share, _ := curve25519.X25519(f.Rand(32), curve25519.Basepoint) + f.X25519RecordIdentity(testkit.TestX25519Identity) + f.X25519Stanza(share, testkit.TestX25519Identity) + body, _ := f.UnreadLine(), f.UnreadLine() + f.TextLine("-> X25519 " + base64.RawStdEncoding.EncodeToString(append(share, 0x00))) + f.TextLine(body) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("a trailing zero is missing from the X25519 share") + f.Generate() +} diff --git a/tests/x25519_low_order.go b/tests/x25519_low_order.go index 2e465bb..b27db75 100644 --- a/tests/x25519_low_order.go +++ b/tests/x25519_low_order.go @@ -6,14 +6,7 @@ package main -import ( - "crypto/sha256" - "encoding/base64" - - "filippo.io/age/internal/testkit" - "golang.org/x/crypto/curve25519" - "golang.org/x/crypto/hkdf" -) +import "filippo.io/age/internal/testkit" func main() { f := testkit.NewTestFile() @@ -24,12 +17,7 @@ func main() { share := []byte{0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0xd7} - f.ArgsLine("X25519", base64.RawStdEncoding.EncodeToString(share)) - secret := make([]byte, curve25519.PointSize) - key := make([]byte, 32) - hkdf.New(sha256.New, secret, append(share, testkit.TestX25519Recipient...), - []byte("age-encryption.org/v1/X25519")).Read(key) - f.AEADBody(key, testkit.TestFileKey) + f.X25519Stanza(share, testkit.TestX25519Identity) f.HMAC() f.Payload("age") f.ExpectHeaderFailure() diff --git a/tests/x25519_lowercase.go b/tests/x25519_lowercase.go new file mode 100644 index 0000000..838690c --- /dev/null +++ b/tests/x25519_lowercase.go @@ -0,0 +1,27 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "strings" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + f.X25519(testkit.TestX25519Recipient) + body, args := f.UnreadLine(), f.UnreadLine() + f.TextLine(strings.Replace(args, "X25519", "x25519", -1)) + f.TextLine(body) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("the first argument in the X25519 stanza is lowercase") + f.Generate() +} diff --git a/tests/x25519_short_share.go b/tests/x25519_short_share.go new file mode 100644 index 0000000..9210a7a --- /dev/null +++ b/tests/x25519_short_share.go @@ -0,0 +1,30 @@ +// Copyright 2022 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "encoding/base64" + "encoding/hex" + + "filippo.io/age/internal/testkit" +) + +func main() { + f := testkit.NewTestFile() + f.VersionLine("v1") + share, _ := hex.DecodeString("97ba38a135fd5f9137fca3836bfec24340ab03d7ca316b26f482636334a52600") + f.X25519RecordIdentity(testkit.TestX25519Identity) + f.X25519Stanza(share, testkit.TestX25519Identity) + body, _ := f.UnreadLine(), f.UnreadLine() + f.TextLine("-> X25519 " + base64.RawStdEncoding.EncodeToString(share[:31])) + f.TextLine(body) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("a trailing zero is missing from the X25519 share") + f.Generate() +}