|
|
|
|
|
package capsule_test
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"crypto/elliptic"
|
|
|
|
|
|
"crypto/sha256"
|
Signature plan, step 6: the fixture format3_signed and the vectors of its signature
format3_signed is written by EncryptFiles with a test key. Its record gives
the seed of the key, control_commit, head_digest, signers_digest,
AUTHOR_MESSAGE with its code, the signature and the content of key 2, and
verdicts carries the dkauthor1 key. The conformance test recomputes all of
it from the control, the head and the security area, and signs again from
the seed. A second test changes the context, a bit of the signature or of
the key, and the key itself, and removes the signature.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
"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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
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())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Signature plan, step 6: the fixture format3_signed and the vectors of its signature
format3_signed is written by EncryptFiles with a test key. Its record gives
the seed of the key, control_commit, head_digest, signers_digest,
AUTHOR_MESSAGE with its code, the signature and the content of key 2, and
verdicts carries the dkauthor1 key. The conformance test recomputes all of
it from the control, the head and the security area, and signs again from
the seed. A second test changes the context, a bit of the signature or of
the key, and the key itself, and removes the signature.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
|
|
|
|
|
|
// 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
|
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
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
|
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
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.
|
|
|
|
|
|
type sealer struct {
|
|
|
|
|
|
tsa cmstest.Signer
|
|
|
|
|
|
when time.Time
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s sealer) Seal(subject [32]byte) ([]byte, error) {
|
|
|
|
|
|
return cmstest.Token(subject[:], s.when, cmstest.TokenOptions{}, 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, 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()
|
|
|
|
|
|
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.Verdicts.Signature != capsule.VerdictNoSignature || o.Verdicts.Seal != capsule.VerdictSealed {
|
|
|
|
|
|
t.Errorf("verdicts %+v", o.Verdicts)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
|
|
|
|
|
|
// 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 nil, the exclusions are checked before a file is
|
|
|
|
|
|
// read, and format 2 refuses the options it cannot honour.
|
|
|
|
|
|
func TestWriterOptionsChecked(t *testing.T) {
|
|
|
|
|
|
opts := files3(t)
|
|
|
|
|
|
opts.AuthorKey = (*authorkey.Key)(nil)
|
|
|
|
|
|
opts.CMSSigner = (*cmsSigner)(nil)
|
|
|
|
|
|
if _, err := capsule.EncryptFiles(io.Discard, []capsule.Source{source("a", "x")}, opts); err != nil {
|
|
|
|
|
|
t.Errorf("typed nils: %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[2JFirmado 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|