diff --git a/capsule/encrypt3.go b/capsule/encrypt3.go index 43ec558..953fffa 100644 --- a/capsule/encrypt3.go +++ b/capsule/encrypt3.go @@ -113,7 +113,7 @@ func EncryptFiles(dst io.Writer, sources []Source, opts EncryptOptions) (*Result return nil, err } security := EncodeSecurity() - if v := EvaluateSecurity(security); v != (Verdicts{VerdictNoSignature, VerdictNoSeal}) { + if v := EvaluateSecurity(security); v != (Verdicts{Signature: VerdictNoSignature, Seal: VerdictNoSeal}) { return nil, fmt.Errorf("capsule: self-check: the reader finds the verdicts %s and %s in this security area", v.Signature, v.Seal) } frame := BodyFrame{AreaLen: AreaLen, SecurityLen: uint32(len(security)), HeadLen: uint32(len(head))} diff --git a/capsule/format3.go b/capsule/format3.go index 0e7a031..13e34c2 100644 --- a/capsule/format3.go +++ b/capsule/format3.go @@ -6,6 +6,7 @@ import ( datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/codec" + "g.activething.com/go/DateKeys/codec/bech32" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/internal/pathrule" ) @@ -111,8 +112,8 @@ func CheckArea(area []byte, securityLen uint32) error { // security area (spec §29.7). A verdict never prevents opening. type Verdict string -// The verdicts this version can reach (spec §29.7). It implements no alg and -// no seal_type. +// The verdicts of a reader (spec v0.11, §29.7). A reader of v0.10 reaches +// X, F0, F1, S0, S1 and S2 only. const ( // VerdictUnreadable (X): security fails its layer 2 or 3; it stands for // both the signature and the seal. @@ -122,6 +123,14 @@ const ( // VerdictSignatureUnchecked (F1): a signature that does not decode, breaks // its schema or has an alg this reader does not implement. VerdictSignatureUnchecked Verdict = "F1" + // VerdictSignatureInvalid (F2): a signature present that does not verify. + VerdictSignatureInvalid Verdict = "F2" + // VerdictSignedSaved (F3): a valid signature of alg 1 with a key the + // person saved, whose label Verdicts.AuthorLabel holds. + VerdictSignedSaved Verdict = "F3" + // VerdictSignedOther (F4): a valid signature of alg 1 with another key, + // which Verdicts.AuthorKey holds. It does not prove who holds it. + VerdictSignedOther Verdict = "F4" // VerdictNoSeal (S0): no key 3; nothing is shown about the date. VerdictNoSeal Verdict = "S0" // VerdictSealUnsupported (S1): a seal_type this reader does not implement. @@ -132,7 +141,8 @@ const ( ) // Text returns the text of the verdict that the official SDK shows, in -// Spanish (spec §29.7), and "" for S0, which shows nothing. +// Spanish (spec §29.7), and "" for S0, which shows nothing, and for the +// verdicts whose text names a key, which Verdicts.Lines writes. func (v Verdict) Text() string { switch v { case VerdictUnreadable: @@ -141,6 +151,8 @@ func (v Verdict) Text() string { return "Sin firma de autor." case VerdictSignatureUnchecked: return "No se ha comprobado ninguna firma: trátala como no firmada." + case VerdictSignatureInvalid: + return "La firma no corresponde a este contenido." case VerdictSealUnsupported: return "Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada." case VerdictSealUnreadable: @@ -152,6 +164,10 @@ func (v Verdict) Text() string { // Verdicts are the verdicts of the security area of a format 3 capsule. type Verdicts struct { Signature, Seal Verdict + // AuthorKey is the public key of a valid signature of alg 1 (F3, F4). + AuthorKey [32]byte + // AuthorLabel is the label of the saved key that signed (F3). + AuthorLabel string } // Lines are the verdicts as the official SDK shows them, in order: X alone, @@ -161,6 +177,13 @@ func (v Verdicts) Lines() []string { return []string{VerdictUnreadable.Text()} } lines := []string{v.Signature.Text()} + switch v.Signature { + case VerdictSignedSaved: + lines[0] = "Firmado con la clave que guardaste como " + v.AuthorLabel + "." + case VerdictSignedOther: + key, _ := bech32.Encode("dkauthor", v.AuthorKey[:]) + lines[0] = "Firmado con la clave " + key + ". No prueba quién la tiene." + } if t := v.Seal.Text(); t != "" { lines = append(lines, t) } @@ -248,34 +271,25 @@ func EncodeSecurityWith(signature, seal []byte) ([]byte, error) { return e.Out() } -// EvaluateSecurity reads SECURITY_CBOR and returns its verdicts (spec §29.3, -// §29.7). It never fails: security never decides the opening. For the +// EvaluateSecurity reads SECURITY_CBOR and returns its verdicts without the +// capsule around it (spec §29.3, §29.7), as a reader of v0.10 does: it checks +// the structure, and any signature is F1. EvaluateSecurityIn checks the +// signature too. It never fails: security never decides the opening. For the // signature and for the seal apart, the first row of the table of §29.7 that // holds decides: alg and seal_type are read only from content that decodes // and meets its schema. -func EvaluateSecurity(b []byte) Verdicts { - x := Verdicts{VerdictUnreadable, VerdictUnreadable} +func EvaluateSecurity(b []byte) Verdicts { return EvaluateSecurityIn(b, nil) } + +// decodeSecurity decodes the outer map of SECURITY_CBOR, layers 2 and 3. +func decodeSecurity(b []byte) (*securityWire, bool) { if tag, version, err := codec.Peek(b); err != nil || tag != SecurityTypeTag || version != SecurityVersion { - return x + return nil, false } var w securityWire if err := codec.Unmarshal(b, w.decode, w.encode); err != nil { - return x - } - v := Verdicts{Signature: VerdictNoSignature, Seal: VerdictNoSeal} - if w.signature != nil { - // A content that does not decode and an alg this version does not - // implement give the same verdict, and this version implements none. - v.Signature = VerdictSignatureUnchecked - } - if w.seal != nil { - if _, err := decodeSeal(w.seal); err != nil { - v.Seal = VerdictSealUnreadable - } else { - v.Seal = VerdictSealUnsupported - } + return nil, false } - return v + return &w, true } // authorSignature is the content of key 2 of security: {0: alg, 1: public diff --git a/capsule/signature.go b/capsule/signature.go new file mode 100644 index 0000000..a1282df --- /dev/null +++ b/capsule/signature.go @@ -0,0 +1,184 @@ +package capsule + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "time" + + "g.activething.com/go/DateKeys/codec/bech32" + "g.activething.com/go/DateKeys/internal/ed25519strict" +) + +// The domain prefixes of what an author signs and a seal seals (spec v0.11, +// §29.8, §29.11). Each is followed by a byte 0x00, except in AUTHOR_MESSAGE, +// where a line feed follows it. +const ( + payloadCommitPrefix = "datekeys:dkc3:payload:v1" + controlCommitPrefix = "datekeys:dkc3:control:v1" + headDigestPrefix = "datekeys:dkc3:head:v1" + signersDigestPrefix = "datekeys:dkc3:signers:v1" + sigPartPrefix = "datekeys:dkc3:sig-part:v1" + sealSubjectPrefix = "datekeys:dkc3:seal-subject:v1" + // AuthorMessagePrefix is the first line of AUTHOR_MESSAGE. + AuthorMessagePrefix = "datekeys:dkc3:author-signature:v1" + // AuthorMessageSize is the length of AUTHOR_MESSAGE: the prefix, a line + // feed, the 64 hexadecimal digits of its digest and a line feed. + AuthorMessageSize = len(AuthorMessagePrefix) + 1 + 64 + 1 +) + +// The values of alg that this version defines (spec v0.11, §29.3). +const ( + // AlgEd25519 is a strict Ed25519 signature with a key of one's own (§29.9). + AlgEd25519 = 1 + // AlgCMS is a CMS signature with X.509 certificates (§29.10). + AlgCMS = 2 +) + +// AuthorKey signs AUTHOR_MESSAGE with alg 1, as *authorkey.Key does. +type AuthorKey interface { + // Public returns the public key A, 32 bytes. + Public() []byte + // Sign returns the Ed25519 signature of message, 64 bytes. + Sign(message []byte) []byte +} + +func domainHash(prefix string, parts ...[]byte) [32]byte { + h := sha256.New() + h.Write([]byte(prefix)) + h.Write([]byte{0}) + for _, p := range parts { + h.Write(p) + } + var out [32]byte + h.Sum(out[:0]) + return out +} + +// PayloadCommit is the commitment to I_PAYLOAD that replaces it in what is +// signed (spec §29.8). +func PayloadCommit(identity [32]byte) [32]byte { + return domainHash(payloadCommitPrefix, identity[:]) +} + +// ControlCommit is control_commit: the hash of CONTROL_SIG, the control of a +// capsule of format f with payload_commit in place of I_PAYLOAD and the eight +// bytes of L at zero (spec §29.8). It does not depend on L, so the area can +// grow after signing. +func ControlCommit(c *Control, f Format) ([32]byte, error) { + sig := *c + sig.PayloadIdentity = PayloadCommit(c.PayloadIdentity) + sig.PayloadLength = 0 + b, err := EncodeControl(&sig, f) + if err != nil { + return [32]byte{}, err + } + return domainHash(controlCommitPrefix, b), nil +} + +// HeadDigest is head_digest, the hash of HEAD_CBOR (spec §29.8). The salt of +// the head makes it a commitment that hides the files. +func HeadDigest(head []byte) [32]byte { return domainHash(headDigestPrefix, head) } + +// SignersDigest is signers_digest for alg and the exact content of key 1 of a +// signature of alg 2, signers; nil with alg 1 (spec §29.8). +func SignersDigest(alg uint32, signers []byte) [32]byte { + return domainHash(signersDigestPrefix, binary.BigEndian.AppendUint32(nil, alg), signers) +} + +// AuthorMessage is AUTHOR_MESSAGE, the ASCII text that an author signs: +// AuthorMessagePrefix, a line feed, the hexadecimal digest D of the three +// commitments and a line feed (spec §29.8). +func AuthorMessage(controlCommit, headDigest, signersDigest [32]byte) []byte { + d := sha256.Sum256(append(append(controlCommit[:], headDigest[:]...), signersDigest[:]...)) + m := make([]byte, 0, AuthorMessageSize) + m = append(m, AuthorMessagePrefix...) + m = append(m, '\n') + m = hex.AppendEncode(m, d[:]) + return append(m, '\n') +} + +// AuthorCode is the code of AUTHOR_MESSAGE that a person compares before +// signing: the first 8 hexadecimal digits of its digest, in two groups of 4. +func AuthorCode(message []byte) string { + if len(message) != AuthorMessageSize { + return "" + } + d := message[len(AuthorMessagePrefix)+1:] + return string(d[:4]) + "-" + string(d[4:8]) +} + +// SigPart is SIG_PART: 0x00 without key 2, and 0x01 and the hash of the exact +// content of key 2 otherwise, whatever its alg and its verdict (spec §29.11). +func SigPart(signature []byte) []byte { + if signature == nil { + return []byte{0} + } + h := domainHash(sigPartPrefix, signature) + return append([]byte{1}, h[:]...) +} + +// SealSubject is SEAL_SUBJECT, what a seal of seal_type 2 seals (spec §29.11). +func SealSubject(controlCommit, headDigest [32]byte, sigPart []byte) [32]byte { + return domainHash(sealSubjectPrefix, controlCommit[:], headDigest[:], sigPart) +} + +// SecurityContext is what the verdicts of a signature or a seal need besides +// SECURITY_CBOR: the commitments of the capsule, the time of its round, and +// the author keys that the person saved, by their dkauthor1… string, with the +// label she gave them (F3). A reader builds it at step 17.6. +type SecurityContext struct { + ControlCommit, HeadDigest [32]byte + RoundTime time.Time + AuthorKeys map[string]string +} + +// EvaluateSecurityIn returns the verdicts of SECURITY_CBOR in the capsule +// that c describes (spec §29.7). Without a context, as EvaluateSecurity, it +// checks only the structure: any signature is F1, as in v0.10. It never +// fails: security never decides the opening. +func EvaluateSecurityIn(b []byte, c *SecurityContext) Verdicts { + x := Verdicts{Signature: VerdictUnreadable, Seal: VerdictUnreadable} + w, ok := decodeSecurity(b) + if !ok { + return x + } + v := Verdicts{Signature: VerdictNoSignature, Seal: VerdictNoSeal} + if w.signature != nil { + v.Signature = VerdictSignatureUnchecked + if c != nil { + evaluateSignature(&v, w.signature, c) + } + } + if w.seal != nil { + if _, err := decodeSeal(w.seal); err != nil { + v.Seal = VerdictSealUnreadable + } else { + v.Seal = VerdictSealUnsupported + } + } + return v +} + +// evaluateSignature sets the verdict of the content of key 2: F1 for content +// that does not decode, an alg this reader does not implement or a key or a +// signature of another length; F2 when the signature does not verify; F3 or +// F4 when it does (spec §29.7, §29.9). +func evaluateSignature(v *Verdicts, content []byte, c *SecurityContext) { + a, err := decodeAuthorSignature(content) + if err != nil || a.alg != AlgEd25519 || len(a.key) != 32 || len(a.value) != 64 { + return + } + msg := AuthorMessage(c.ControlCommit, c.HeadDigest, SignersDigest(AlgEd25519, nil)) + if !ed25519strict.Verify(a.key, msg, a.value) { + v.Signature = VerdictSignatureInvalid + return + } + v.Signature = VerdictSignedOther + copy(v.AuthorKey[:], a.key) + if s, err := bech32.Encode("dkauthor", a.key); err == nil { + if label, ok := c.AuthorKeys[s]; ok { + v.Signature, v.AuthorLabel = VerdictSignedSaved, label + } + } +} diff --git a/capsule/signature_test.go b/capsule/signature_test.go new file mode 100644 index 0000000..d0a0009 --- /dev/null +++ b/capsule/signature_test.go @@ -0,0 +1,97 @@ +package capsule_test + +import ( + "bytes" + "strings" + "testing" + + "g.activething.com/go/DateKeys/authorkey" + "g.activething.com/go/DateKeys/capsule" +) + +// Spec v0.11 §29.8: AUTHOR_MESSAGE is ASCII text of 99 bytes with its code, +// and control_commit does not depend on L. +func TestAuthorMessage(t *testing.T) { + var cc, hd [32]byte + cc[0], hd[0] = 1, 2 + m := capsule.AuthorMessage(cc, hd, capsule.SignersDigest(capsule.AlgEd25519, nil)) + if capsule.AuthorMessageSize != 99 || len(m) != 99 || !bytes.HasPrefix(m, []byte(capsule.AuthorMessagePrefix+"\n")) || m[98] != '\n' { + t.Fatalf("AUTHOR_MESSAGE %q", m) + } + if code := capsule.AuthorCode(m); len(code) != 9 || code[4] != '-' || code[:4] != string(m[34:38]) { + t.Errorf("code %q", code) + } + if capsule.SignersDigest(capsule.AlgEd25519, nil) == capsule.SignersDigest(capsule.AlgCMS, nil) { + t.Error("signers_digest does not bind alg") + } + c := &capsule.Control{PayloadLength: 100, Padding: capsule.Reforzado} + c.PayloadIdentity[0] = 7 + a, err := capsule.ControlCommit(c, capsule.Format3) + if err != nil { + t.Fatal(err) + } + c.PayloadLength = 1 << 40 + if b, _ := capsule.ControlCommit(c, capsule.Format3); b != a { + t.Error("control_commit depends on L") + } + c.PayloadIdentity[0] = 8 + if b, _ := capsule.ControlCommit(c, capsule.Format3); b == a { + t.Error("control_commit does not depend on I_PAYLOAD") + } + if capsule.SigPart(nil)[0] != 0 || len(capsule.SigPart([]byte{1})) != 33 { + t.Error("SIG_PART") + } +} + +// Spec v0.11 §29.7, §29.9: a signature of alg 1 gives F4, or F3 with a saved +// key; F2 when it does not verify; F1 without the capsule around it, as in +// v0.10, and for what this reader does not implement. +func TestEvaluateSecurityIn(t *testing.T) { + key, _ := authorkey.Generate() + ctx := &capsule.SecurityContext{} + ctx.ControlCommit[0], ctx.HeadDigest[0] = 1, 2 + msg := capsule.AuthorMessage(ctx.ControlCommit, ctx.HeadDigest, capsule.SignersDigest(capsule.AlgEd25519, nil)) + security := func(alg uint64, pub, sig []byte) []byte { + content, err := capsule.EncodeAuthorSignature(alg, pub, sig) + if err != nil { + t.Fatal(err) + } + b, err := capsule.EncodeSecurityWith(content, nil) + if err != nil { + t.Fatal(err) + } + return b + } + signed := security(capsule.AlgEd25519, key.Public(), key.Sign(msg)) + v := capsule.EvaluateSecurityIn(signed, ctx) + if v.Signature != capsule.VerdictSignedOther || !bytes.Equal(v.AuthorKey[:], key.Public()) { + t.Fatalf("verdicts %+v", v) + } + pub, _ := authorkey.PublicString(key.Public()) + if line := v.Lines()[0]; !strings.Contains(line, pub) || !strings.HasSuffix(line, "No prueba quién la tiene.") { + t.Errorf("F4 line %q", line) + } + ctx.AuthorKeys = map[string]string{pub: "Ana"} + if v := capsule.EvaluateSecurityIn(signed, ctx); v.Signature != capsule.VerdictSignedSaved || v.Lines()[0] != "Firmado con la clave que guardaste como Ana." { + t.Errorf("F3: %+v %q", v, v.Lines()) + } + other := *ctx + other.HeadDigest[0] = 3 + if v := capsule.EvaluateSecurityIn(signed, &other); v.Signature != capsule.VerdictSignatureInvalid { + t.Errorf("another head: %s", v.Signature) + } + for name, b := range map[string][]byte{ + "no context": signed, + "a key of 31": security(capsule.AlgEd25519, key.Public()[:31], key.Sign(msg)), + "alg 2, not yet": security(capsule.AlgCMS, []byte{1}, []byte{1}), + "an unknown alg 9": security(9, key.Public(), key.Sign(msg)), + } { + c := ctx + if name == "no context" { + c = nil + } + if v := capsule.EvaluateSecurityIn(b, c); v.Signature != capsule.VerdictSignatureUnchecked { + t.Errorf("%s: %s", name, v.Signature) + } + } +}