diff --git a/internal/testkit/testkit.go b/internal/testkit/testkit.go index fd37579..b2410ae 100644 --- a/internal/testkit/testkit.go +++ b/internal/testkit/testkit.go @@ -30,6 +30,16 @@ var _, TestX25519Identity, _ = bech32.Decode( var TestX25519Recipient, _ = curve25519.X25519(TestX25519Identity, curve25519.Basepoint) +func NotCanonicalBase64(s string) string { + // Assuming there are spare zero bits at the end of the encoded bitstring, + // the character immediately after in the alphabet compared to the last one + // in the encoding will only flip the last bit to one, making the string a + // non-canonical encoding of the same value. + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + idx := strings.IndexByte(alphabet, s[len(s)-1]) + return s[:len(s)-1] + string(alphabet[idx+1]) +} + type TestFile struct { Buf bytes.Buffer Rand func(n int) []byte @@ -64,6 +74,14 @@ func (f *TestFile) TextLine(s string) { f.Buf.WriteString("\n") } +func (f *TestFile) UnreadLine() string { + buf := bytes.TrimSuffix(f.Buf.Bytes(), []byte("\n")) + idx := bytes.LastIndex(buf[:len(buf)-1], []byte("\n")) + 1 + f.Buf.Reset() + f.Buf.Write(buf[:idx]) + return string(buf[idx:]) +} + func (f *TestFile) VersionLine(v string) { f.TextLine("age-encryption.org/" + v) } diff --git a/testdata/testkit/crlf b/testdata/testkit/crlf new file mode 100644 index 0000000..69cafae --- /dev/null +++ b/testdata/testkit/crlf @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 +comment: lines in the header end with CRLF instead of LF + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- 2KIGb7ye32MWtUuEVWkO3MP6qCDLzOvT9wF06lelBSI +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_bad b/testdata/testkit/hmac_bad index dd37b49..59168ce 100644 --- a/testdata/testkit/hmac_bad +++ b/testdata/testkit/hmac_bad @@ -5,5 +5,5 @@ identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P age-encryption.org/v1 -> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE ---- UGnodA32FkH3AOl9BHP6biSZTkNvu8B99I++7JpqZGk -[. #wυ=aYkz66ڦRL \ No newline at end of file +--- 8McE3ix9R34E/vLrQv3yepsHjo/LXhfs22Ab3UyInmg +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_extra_space b/testdata/testkit/hmac_extra_space new file mode 100644 index 0000000..f3bfd1e --- /dev/null +++ b/testdata/testkit/hmac_extra_space @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- WyJp9F/9FOZh7gJdheq2WIJcwHgYc8NIVh3ddwhrcNg +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_garbage b/testdata/testkit/hmac_garbage new file mode 100644 index 0000000..29e54ab --- /dev/null +++ b/testdata/testkit/hmac_garbage @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- WyJp9F/9FOZh7gJdheq2WIJcwHgYc8NIVh3ddwhrcNgAAA +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_missing b/testdata/testkit/hmac_missing new file mode 100644 index 0000000..6ecea2a --- /dev/null +++ b/testdata/testkit/hmac_missing @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_no_space b/testdata/testkit/hmac_no_space new file mode 100644 index 0000000..9f1d80d --- /dev/null +++ b/testdata/testkit/hmac_no_space @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +---WyJp9F/9FOZh7gJdheq2WIJcwHgYc8NIVh3ddwhrcNg +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_not_canonical b/testdata/testkit/hmac_not_canonical new file mode 100644 index 0000000..fd18cda --- /dev/null +++ b/testdata/testkit/hmac_not_canonical @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 +comment: the base64 encoding of the HMAC is not canonical + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- WyJp9F/9FOZh7gJdheq2WIJcwHgYc8NIVh3ddwhrcNh +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_trailing_space b/testdata/testkit/hmac_trailing_space new file mode 100644 index 0000000..2e216bd --- /dev/null +++ b/testdata/testkit/hmac_trailing_space @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- WyJp9F/9FOZh7gJdheq2WIJcwHgYc8NIVh3ddwhrcNg +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/hmac_truncated b/testdata/testkit/hmac_truncated new file mode 100644 index 0000000..e7f7196 --- /dev/null +++ b/testdata/testkit/hmac_truncated @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +hjabGXwSLQ9c3S6Lw2i+S2Tu2fiwQHHslbBN6B41FLE +--- WyJp +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/x25519_extra_argument b/testdata/testkit/x25519_extra_argument new file mode 100644 index 0000000..198389b --- /dev/null +++ b/testdata/testkit/x25519_extra_argument @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 +comment: the base64 encoding of the share is not canonical + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc 1234 +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +--- hQQySEUXL8pOuIOuw0qXzi66RphDJP9IKMNEChNJIPk +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/x25519_low_order b/testdata/testkit/x25519_low_order index 84e9fa3..f528a0d 100644 --- a/testdata/testkit/x25519_low_order +++ b/testdata/testkit/x25519_low_order @@ -1,7 +1,7 @@ expect: header failure file key: 59454c4c4f57205355424d4152494e45 identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 -comment: the X25519 share is a low-order point, so the shared secret is the disallowed all-zero value +comment: the X25519 share is a low-order point, so the shared secretis the disallowed all-zero value age-encryption.org/v1 -> X25519 X5yVvKNQjCSx0LFVnIPvWwREXMRYHI6G2CJO3dCfEdc diff --git a/testdata/testkit/x25519_not_canonical_body b/testdata/testkit/x25519_not_canonical_body new file mode 100644 index 0000000..11138db --- /dev/null +++ b/testdata/testkit/x25519_not_canonical_body @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 +comment: the base64 encoding of the share is not canonical + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7V +--- eSjjCjQyp30yHDPwCztKS+1txs+aoCa5ERz8jeEp+9A +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/testdata/testkit/x25519_not_canonical_share b/testdata/testkit/x25519_not_canonical_share new file mode 100644 index 0000000..0b2a08d --- /dev/null +++ b/testdata/testkit/x25519_not_canonical_share @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6 +comment: the base64 encoding of the share is not canonical + +age-encryption.org/v1 +-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCd +EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U +--- AO6haEGU6BGJ8Tzeqnr2fSLEo31JrWodGtZuCZmijI8 +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/tests/crlf.go b/tests/crlf.go new file mode 100644 index 0000000..721626b --- /dev/null +++ b/tests/crlf.go @@ -0,0 +1,29 @@ +// 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.TestX25519Identity) + hdr := f.Buf.Bytes() + f.Buf.Reset() + f.Buf.Write(bytes.Replace(hdr, []byte("\n"), []byte("\r\n"), -1)) + f.HMAC() + f.Buf.WriteString(f.UnreadLine()) + f.Buf.WriteString("\r\n") + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("lines in the header end with CRLF instead of LF") + f.Generate() +} diff --git a/tests/hmac_bad.go b/tests/hmac_bad.go index 15c2854..a8556f0 100644 --- a/tests/hmac_bad.go +++ b/tests/hmac_bad.go @@ -12,7 +12,7 @@ func main() { f := testkit.NewTestFile() f.VersionLine("v1") f.X25519(testkit.TestX25519Identity) - f.FileKey(f.Rand(16)) + f.FileKey(make([]byte, 16)) f.HMAC() f.FileKey(testkit.TestFileKey) f.Payload("age") diff --git a/tests/hmac_extra_space.go b/tests/hmac_extra_space.go new file mode 100644 index 0000000..6d88871 --- /dev/null +++ b/tests/hmac_extra_space.go @@ -0,0 +1,24 @@ +// 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.TestX25519Identity) + f.HMAC() + f.TextLine(strings.Replace(f.UnreadLine(), "--- ", "--- ", -1)) + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/hmac_garbage.go b/tests/hmac_garbage.go new file mode 100644 index 0000000..04e4df5 --- /dev/null +++ b/tests/hmac_garbage.go @@ -0,0 +1,20 @@ +// 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.TestX25519Identity) + f.HMAC() + f.TextLine(f.UnreadLine() + "AAA") + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/hmac_missing.go b/tests/hmac_missing.go new file mode 100644 index 0000000..b094de8 --- /dev/null +++ b/tests/hmac_missing.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("v1") + f.X25519(testkit.TestX25519Identity) + f.HMACLine(nil) + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/hmac_no_space.go b/tests/hmac_no_space.go new file mode 100644 index 0000000..d15c9df --- /dev/null +++ b/tests/hmac_no_space.go @@ -0,0 +1,24 @@ +// 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.TestX25519Identity) + f.HMAC() + f.TextLine(strings.Replace(f.UnreadLine(), "--- ", "---", -1)) + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/hmac_not_canonical.go b/tests/hmac_not_canonical.go new file mode 100644 index 0000000..0f6fc06 --- /dev/null +++ b/tests/hmac_not_canonical.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.TestX25519Identity) + f.HMAC() + f.TextLine(testkit.NotCanonicalBase64(f.UnreadLine())) + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("the base64 encoding of the HMAC is not canonical") + f.Generate() +} diff --git a/tests/hmac_trailing_space.go b/tests/hmac_trailing_space.go new file mode 100644 index 0000000..02395d7 --- /dev/null +++ b/tests/hmac_trailing_space.go @@ -0,0 +1,20 @@ +// 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.TestX25519Identity) + f.HMAC() + f.TextLine(f.UnreadLine() + " ") + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/hmac_truncated.go b/tests/hmac_truncated.go new file mode 100644 index 0000000..9f3a009 --- /dev/null +++ b/tests/hmac_truncated.go @@ -0,0 +1,20 @@ +// 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.TestX25519Identity) + f.HMAC() + f.TextLine(f.UnreadLine()[:len("--- 1234")]) + f.Payload("age") + f.ExpectHeaderFailure() + f.Generate() +} diff --git a/tests/x25519_extra_argument.go b/tests/x25519_extra_argument.go new file mode 100644 index 0000000..2974b0e --- /dev/null +++ b/tests/x25519_extra_argument.go @@ -0,0 +1,23 @@ +// 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) + body, args := f.UnreadLine(), f.UnreadLine() + f.TextLine(args + " 1234") + f.TextLine(body) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("the base64 encoding of the share is not canonical") + f.Generate() +} diff --git a/tests/x25519_low_order.go b/tests/x25519_low_order.go index fbc374a..2e465bb 100644 --- a/tests/x25519_low_order.go +++ b/tests/x25519_low_order.go @@ -33,6 +33,7 @@ func main() { f.HMAC() f.Payload("age") f.ExpectHeaderFailure() - f.Comment("the X25519 share is a low-order point, so the shared secret is the disallowed all-zero value") + f.Comment("the X25519 share is a low-order point, so the shared secret" + + "is the disallowed all-zero value") f.Generate() } diff --git a/tests/x25519_not_canonical_body.go b/tests/x25519_not_canonical_body.go new file mode 100644 index 0000000..ee49f96 --- /dev/null +++ b/tests/x25519_not_canonical_body.go @@ -0,0 +1,23 @@ +// 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) + body, args := f.UnreadLine(), f.UnreadLine() + f.TextLine(args) + f.TextLine(testkit.NotCanonicalBase64(body)) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("the base64 encoding of the share is not canonical") + f.Generate() +} diff --git a/tests/x25519_not_canonical_share.go b/tests/x25519_not_canonical_share.go new file mode 100644 index 0000000..d9696ca --- /dev/null +++ b/tests/x25519_not_canonical_share.go @@ -0,0 +1,23 @@ +// 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) + body, args := f.UnreadLine(), f.UnreadLine() + f.TextLine(testkit.NotCanonicalBase64(args)) + f.TextLine(body) + f.HMAC() + f.Payload("age") + f.ExpectHeaderFailure() + f.Comment("the base64 encoding of the share is not canonical") + f.Generate() +}