You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/capsule/signed_test.go

455 lines
17 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

package capsule_test
import (
"bytes"
"context"
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
"io"
"strings"
"testing"
"time"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/internal/testkit"
)
// openSigned opens dkc with the author keys that the person saved.
func openSigned(t *testing.T, dkc []byte, saved map[string]string) *capsule.Opened {
t.Helper()
o := defaultOpen(1000)
o.Sink, o.AuthorKeys = &testkit.MemorySink{}, saved
opened, err := capsule.Open(context.Background(), nil, bytes.NewReader(dkc), o)
if err != nil {
t.Fatal(err)
}
return opened
}
// Spec v0.11 §29.7, §29.8, §62.1 rule 19: EncryptFiles signs with the key of
// opts.AuthorKey and Open gives F4, or F3 with the key saved.
func TestEncryptFilesSigned(t *testing.T) {
key, err := authorkey.Generate()
if err != nil {
t.Fatal(err)
}
pub, _ := authorkey.PublicString(key.Public())
opts := files3(t)
opts.AuthorKey = key
var dkc bytes.Buffer
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
t.Fatal(err)
}
o := openSigned(t, dkc.Bytes(), nil)
if o.Verdicts.Signature != capsule.VerdictSignedOther || o.Verdicts.AuthorKey != [32]byte(key.Public()) || o.Verdicts.Seal != capsule.VerdictNoSeal || o.AreaLen != capsule.AreaLen {
t.Errorf("verdicts %+v, area %d", o.Verdicts, o.AreaLen)
}
o = openSigned(t, dkc.Bytes(), map[string]string{pub: "Ana"})
if o.Verdicts.Signature != capsule.VerdictSignedSaved || o.Verdicts.AuthorLabel != "Ana" {
t.Errorf("saved key: verdicts %+v", o.Verdicts)
}
other, _ := authorkey.Generate()
otherPub, _ := authorkey.PublicString(other.Public())
if o = openSigned(t, dkc.Bytes(), map[string]string{otherPub: "Luis"}); o.Verdicts.Signature != capsule.VerdictSignedOther {
t.Errorf("another saved key: verdicts %+v", o.Verdicts)
}
// LargeArea only allows widening: a signature that fits keeps the area of 32 KiB.
opts.LargeArea = true
dkc.Reset()
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
t.Fatal(err)
}
if o = openSigned(t, dkc.Bytes(), nil); o.AreaLen != capsule.AreaLen || o.Verdicts.Signature != capsule.VerdictSignedOther {
t.Errorf("large area: verdicts %+v, area %d", o.Verdicts, o.AreaLen)
}
}
// badKey is an AuthorKey that signs wrongly or has a public key of the wrong
// length.
type badKey struct {
pub, sig []byte
}
func (k badKey) Public() []byte { return k.pub }
func (k badKey) Sign([]byte) []byte { return k.sig }
// Spec v0.11 §62.1 rule 19: a signature that does not verify, or a key of
// another length, fails before anything is written.
func TestEncryptFilesSignatureChecked(t *testing.T) {
key, _ := authorkey.Generate()
for _, tc := range []struct {
name string
key capsule.AuthorKey
want string
}{
{"wrong signature", badKey{key.Public(), make([]byte, 64)}, "self-check"},
{"short key", badKey{key.Public()[:31], make([]byte, 64)}, "not 32"},
} {
opts := files3(t)
opts.AuthorKey = tc.key
var dkc bytes.Buffer
_, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "x")}, opts)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Errorf("%s: %v", tc.name, err)
}
if dkc.Len() != 0 {
t.Errorf("%s: %d bytes written", tc.name, dkc.Len())
}
}
}
// Spec v0.11 §29.7, §29.8: a signature of the fixture format3_signed holds
// in the context of its capsule and in no other: a bit of the signature, of
// a commitment or of the message changes the verdict to F2; the same message
// signed by another key is another key's F4; and a security area without it
// is F0.
func TestSignedFixtureVerdicts(t *testing.T) {
f := loadFixture(t, "format3_signed")
body := f.plaintext
frame, err := capsule.ParseBodyFrame(body[:capsule.BodyFrameSize], uint64(len(body)))
if err != nil {
t.Fatal(err)
}
security := body[capsule.BodyFrameSize : capsule.BodyFrameSize+frame.SecurityLen]
cb, _ := hex.DecodeString(f.ControlCBOR)
c, err := capsule.DecodeControl(cb, f.format())
if err != nil {
t.Fatal(err)
}
cc, err := capsule.ControlCommit(c, f.format())
if err != nil {
t.Fatal(err)
}
hb := body[capsule.BodyFrameSize+frame.AreaLen : capsule.BodyFrameSize+frame.AreaLen+frame.HeadLen]
ctx := func() *capsule.SecurityContext {
return &capsule.SecurityContext{ControlCommit: cc, HeadDigest: capsule.HeadDigest(hb)}
}
if v := capsule.EvaluateSecurityIn(security, ctx()); v.Signature != capsule.VerdictSignedOther || v.Seal != capsule.VerdictNoSeal {
t.Fatalf("the signature of the fixture: %+v", v)
}
// Another capsule: another control commitment, or another head.
other := ctx()
other.ControlCommit[0] ^= 1
if v := capsule.EvaluateSecurityIn(security, other); v.Signature != capsule.VerdictSignatureInvalid {
t.Errorf("another control: %+v", v)
}
other = ctx()
other.HeadDigest[31] ^= 1
if v := capsule.EvaluateSecurityIn(security, other); v.Signature != capsule.VerdictSignatureInvalid {
t.Errorf("another head: %+v", v)
}
// A bit of the signature, or of the key.
_, value, err := capsule.SecurityKey2(security)
if err != nil {
t.Fatal(err)
}
pub, _ := authorkey.ParsePublic(f.Signature.AuthorKey)
for name, mutate := range map[string]func(sig, key []byte){
"signature": func(sig, key []byte) { sig[63] ^= 1 },
"key": func(sig, key []byte) { key[0] ^= 1 },
} {
sig, key := bytes.Clone(value), bytes.Clone(pub)
mutate(sig, key)
x, err := capsule.EncodeAuthorSignature(capsule.AlgEd25519, key, sig)
if err != nil {
t.Fatal(err)
}
s, err := capsule.EncodeSecurityWith(x, nil)
if err != nil {
t.Fatal(err)
}
if v := capsule.EvaluateSecurityIn(s, ctx()); v.Signature != capsule.VerdictSignatureInvalid {
t.Errorf("a bit of the %s: %+v", name, v)
}
}
// The same message signed by another key: F4 with that key.
msg := capsule.AuthorMessage(cc, capsule.HeadDigest(hb), capsule.SignersDigest(capsule.AlgEd25519, nil))
k, _ := authorkey.Generate()
x, _ := capsule.EncodeAuthorSignature(capsule.AlgEd25519, k.Public(), k.Sign(msg))
s, _ := capsule.EncodeSecurityWith(x, nil)
if v := capsule.EvaluateSecurityIn(s, ctx()); v.Signature != capsule.VerdictSignedOther || v.AuthorKey != [32]byte(k.Public()) {
t.Errorf("another key: %+v", v)
}
// Removed: F0.
if v := capsule.EvaluateSecurityIn(capsule.EncodeSecurity(), ctx()); v.Signature != capsule.VerdictNoSignature {
t.Errorf("removed: %+v", v)
}
}
// cmsSigner is a CMSSigner that signs as a signing application would: its
// certificates sign AUTHOR_MESSAGE, and the authority tsa seals each
// signature at when.
type cmsSigner struct {
signers []cmstest.Signer
tsa cmstest.Signer
when time.Time
seen []byte // the message it was asked to sign
calls int
junk int // bytes of an unsigned attribute that decides nothing
}
func (c *cmsSigner) Signers() (out [][32]byte) {
for _, s := range c.signers {
out = append(out, sha256.Sum256(s.Cert.Raw))
}
return out
}
func (c *cmsSigner) Sign(message []byte) ([]byte, error) {
c.seen = message
c.calls++
opts := cmstest.Options{Junk: c.junk}
if c.tsa.Key != nil {
opts.Token = func(sig []byte) []byte {
return cmstest.Token(sig, c.when, cmstest.TokenOptions{Accuracy: time.Second}, c.tsa)
}
}
return cmstest.Signature(message, opts, c.signers...), nil
}
// sealer is a Sealer that asks the authority tsa, whose tokens carry an
// accuracy of a second unless noAccuracy.
type sealer struct {
tsa cmstest.Signer
when time.Time
noAccuracy bool
}
func (s sealer) Seal(subject [32]byte) ([]byte, error) {
o := cmstest.TokenOptions{Accuracy: time.Second}
if s.noAccuracy {
o = cmstest.TokenOptions{}
}
return cmstest.Token(subject[:], s.when, o, s.tsa), nil
}
// Spec v0.11 §29.10, §29.11, §62.1 rules 19 and 21: EncryptFiles gives
// AUTHOR_MESSAGE to the CMSSigner, checks that what it returns is complete,
// and Open gives F6; a Sealer seals SEAL_SUBJECT and Open gives S4.
func TestEncryptFilesCMSAndSeal(t *testing.T) {
from, to := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
ana := cmstest.NewECDSA("Ana López", elliptic.P256(), from, to)
luis := cmstest.NewRSA("Luis Gómez", 2048, from, to)
tsa := cmstest.NewECDSA("TSA de prueba", elliptic.P256(), from, to)
opts := files3(t)
when := opts.Now()
signer := &cmsSigner{signers: []cmstest.Signer{ana, luis}, tsa: tsa, when: when}
opts.CMSSigner = signer
var dkc bytes.Buffer
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
t.Fatal(err)
}
o := openSigned(t, dkc.Bytes(), nil)
if o.Verdicts.Signature != capsule.VerdictSignedComplete || o.Verdicts.Seal != capsule.VerdictNoSeal || len(o.Verdicts.Detail.Signers) != 2 ||
len(signer.seen) != capsule.AuthorMessageSize || capsule.AuthorCode(signer.seen) == "" {
t.Errorf("verdicts %+v, message %q", o.Verdicts, signer.seen)
}
if !o.Verdicts.Detail.Signers[0].Before {
t.Error("the seal does not precede the round time")
}
// A signature that lacks a required signer is not written.
third := cmstest.NewECDSA("Falta", elliptic.P256(), from, to)
missing := &cmsSigner{signers: []cmstest.Signer{ana}, tsa: tsa, when: when}
opts.CMSSigner = &requiring{missing, third}
dkc.Reset()
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "x")}, opts); err == nil || !strings.Contains(err.Error(), "F5") || dkc.Len() != 0 {
t.Errorf("a missing signer: %v, %d bytes written", err, dkc.Len())
}
// Without seals the signature is incomplete too.
opts.CMSSigner = &cmsSigner{signers: []cmstest.Signer{ana}}
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "x")}, opts); err == nil || !strings.Contains(err.Error(), "without seal") {
t.Errorf("no seal: %v", err)
}
// A seal over the signature of an author key.
key, _ := authorkey.Generate()
opts.CMSSigner, opts.AuthorKey, opts.Sealer = nil, key, sealer{tsa: tsa, when: when}
dkc.Reset()
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
t.Fatal(err)
}
o = openSigned(t, dkc.Bytes(), nil)
if o.Verdicts.Signature != capsule.VerdictSignedOther || o.Verdicts.Seal != capsule.VerdictSealed || o.Verdicts.Detail.SealHolder != "TSA de prueba" {
t.Errorf("verdicts %+v", o.Verdicts)
}
// And a seal over a capsule without a signature.
opts.AuthorKey = nil
dkc.Reset()
res, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts)
if err != nil {
t.Fatal(err)
}
if o = openSigned(t, dkc.Bytes(), nil); o.Verdicts.Signature != capsule.VerdictNoSignature || o.Verdicts.Seal != capsule.VerdictSealed || res.Security.Seal != capsule.VerdictSealed {
t.Errorf("verdicts %+v, written %+v", o.Verdicts, res.Security)
}
// A seal without accuracy is written, and Result.Security says that it
// proves nothing before the opening date, so that the writer warns of it
// (spec v0.16, §62.1 rule 19).
opts.Sealer = sealer{tsa: tsa, when: when, noAccuracy: true}
dkc.Reset()
if res, err = capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
t.Fatal(err)
}
if res.Security.Seal != capsule.VerdictSealedLate || res.Security.Detail.SealReason != capsule.ReasonNoAccuracy {
t.Errorf("a seal without accuracy: written %+v", res.Security)
}
opts.Sealer = sealer{tsa: tsa, when: when}
// The exclusions.
opts.AuthorKey, opts.CMSSigner = key, signer
if _, err := capsule.EncryptFiles(io.Discard, []capsule.Source{source("a", "x")}, opts); err == nil {
t.Error("AuthorKey and CMSSigner")
}
opts.AuthorKey = nil
if _, err := capsule.EncryptFiles(io.Discard, []capsule.Source{source("a", "x")}, opts); err == nil {
t.Error("CMSSigner and Sealer")
}
}
// requiring asks for one signer more than signs.
type requiring struct {
*cmsSigner
extra cmstest.Signer
}
func (r *requiring) Signers() [][32]byte {
return append(r.cmsSigner.Signers(), sha256.Sum256(r.extra.Cert.Raw))
}
// Review: what is signed does not depend on the area, so the area is chosen
// after the signature, and widening it never makes anybody sign twice. A
// signature that fits keeps the common area, LargeArea or not.
func TestAreaChosenAfterSigning(t *testing.T) {
from, to := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
ana := cmstest.NewECDSA("Ana López", elliptic.P256(), from, to)
tsa := cmstest.NewECDSA("TSA de prueba", elliptic.P256(), from, to)
opts := files3(t)
when := opts.Now()
// 40 KB of signature: too much for 32 KiB, and the person signs once.
big := &cmsSigner{signers: []cmstest.Signer{ana}, tsa: tsa, when: when, junk: 40 << 10}
opts.CMSSigner = big
if _, err := capsule.EncryptFiles(io.Discard, []capsule.Source{source("nota.txt", "x")}, opts); err == nil || !strings.Contains(err.Error(), "LargeArea") || big.calls != 1 {
t.Errorf("without LargeArea: %v after %d calls", err, big.calls)
}
opts.LargeArea = true
big.calls = 0
var dkc bytes.Buffer
res, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts)
if err != nil || big.calls != 1 {
t.Fatalf("with LargeArea: %v after %d calls", err, big.calls)
}
o := openSigned(t, dkc.Bytes(), nil)
if o.AreaLen != capsule.LargeAreaLen || o.Verdicts.Signature != capsule.VerdictSignedComplete || o.PayloadLength != res.Length || o.PaddedLength != res.PaddedLength {
t.Errorf("area %d, verdicts %+v, L %d P %d", o.AreaLen, o.Verdicts, o.PayloadLength, o.PaddedLength)
}
// An unsigned capsule with LargeArea keeps the common area: P does not
// tell that someone asked.
plain := files3(t)
plain.LargeArea = true
dkc.Reset()
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "x")}, plain); err != nil {
t.Fatal(err)
}
if o := openSigned(t, dkc.Bytes(), nil); o.AreaLen != capsule.AreaLen {
t.Errorf("an unsigned capsule with LargeArea: area %d", o.AreaLen)
}
}
// Review: a typed nil is an error, never a capsule without the signature or
// the seal that was asked for; the exclusions are checked before a file is
// read, and format 2 refuses the options it cannot honour.
func TestWriterOptionsChecked(t *testing.T) {
for _, set := range []func(*capsule.EncryptOptions){
func(o *capsule.EncryptOptions) { o.AuthorKey = (*authorkey.Key)(nil) },
func(o *capsule.EncryptOptions) { o.CMSSigner = (*cmsSigner)(nil) },
func(o *capsule.EncryptOptions) { o.Sealer = (*sealer)(nil) },
} {
opts := files3(t)
set(&opts)
if _, err := capsule.EncryptFiles(io.Discard, []capsule.Source{source("a", "x")}, opts); err == nil || !strings.Contains(err.Error(), "holds a nil") {
t.Errorf("a typed nil: %v", err)
}
}
key, _ := authorkey.Generate()
opts := files3(t)
opts.AuthorKey = key
opts.CMSSigner = &cmsSigner{}
opened := false
src := capsule.Source{Path: "a", Size: 1, Open: func() (io.ReadCloser, error) {
opened = true
return io.NopCloser(strings.NewReader("x")), nil
}}
if _, err := capsule.EncryptFiles(io.Discard, []capsule.Source{src}, opts); err == nil || opened {
t.Errorf("AuthorKey and CMSSigner: %v, source opened %v", err, opened)
}
v2 := past(t, 1000)
v2.AuthorKey = key
if _, err := capsule.Encrypt(io.Discard, strings.NewReader("x"), func() capsule.EncryptOptions { v2.Length = 1; return v2 }()); err == nil {
t.Error("format 2 took an AuthorKey")
}
}
// Review: the issuer of a certificate is text of the certificate, and no
// escape, control or bidi character of it reaches the lines of the verdicts.
func TestIssuerTextFiltered(t *testing.T) {
from, to := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
evil := cmstest.NewECDSA("Ana\n\x1b[2J‮Firmado con la clave que guardaste como Banco.", elliptic.P256(), from, to)
tsa := cmstest.NewECDSA("TSA", elliptic.P256(), from, to)
opts := files3(t)
opts.CMSSigner = &cmsSigner{signers: []cmstest.Signer{evil}, tsa: tsa, when: opts.Now()}
var dkc bytes.Buffer
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("a", "x")}, opts); err != nil {
t.Fatal(err)
}
o := openSigned(t, dkc.Bytes(), nil)
if o.Verdicts.Signature != capsule.VerdictSignedComplete {
t.Fatalf("verdicts %+v", o.Verdicts)
}
for _, line := range o.Verdicts.Lines() {
if strings.ContainsAny(line, "\x1b‮⁦⁧⁨⁩\r") || strings.Contains(strings.TrimSuffix(line, "\n"), "\n") {
t.Errorf("a line with a control or a bidi character: %q", line)
}
}
}
// writeWatch is an AuthorKey that records whether its capsule had any byte
// written when it was asked to sign.
type writeWatch struct {
capsule.AuthorKey
dst *bytes.Buffer
written bool
}
func (w *writeWatch) Sign(message []byte) []byte {
w.written = w.dst.Len() > 0
return w.AuthorKey.Sign(message)
}
// Spec §62.1 rules 19 and 25: nothing of the capsule is written while the
// writer waits for a signature, and the signature is checked before.
func TestNothingWrittenBeforeTheSignature(t *testing.T) {
key, _ := authorkey.Generate()
var dkc bytes.Buffer
w := &writeWatch{AuthorKey: key, dst: &dkc}
opts := files3(t)
opts.AuthorKey = w
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("a", "x")}, opts); err != nil {
t.Fatal(err)
}
if w.written || dkc.Len() == 0 {
t.Errorf("bytes written before the signature: %v; capsule of %d bytes", w.written, dkc.Len())
}
}

Powered by TurnKey Linux.