Signature plan, steps 3 and 4 begun: what is signed and the verdicts of alg 1

capsule/signature.go has what an author signs and a seal seals, as the
spec v0.11 draft defines it (29.8, 29.11): payload_commit, control_commit
over CONTROL_SIG, without I_PAYLOAD and with L at zero, head_digest,
signers_digest, AUTHOR_MESSAGE as ASCII text of 99 bytes with its code,
SIG_PART over the exact content of key 2, and SEAL_SUBJECT.

EvaluateSecurityIn checks a signature of alg 1 with the strict profile in
the context of a capsule: F4, or F3 with a key the person saved; F2 when
it does not verify; F1 without context, as in v0.10, and for alg 2, not
yet implemented. Verdicts carries the key and the label for the texts.

Not wired yet: the writer does not sign and still writes the area of 512
bytes, and the reader still calls EvaluateSecurity without context. The
handoff of docs lists what is left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 6 days ago
parent 7e7681e737
commit 3d85a0b857

@ -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))}

@ -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

@ -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
}
}
}

@ -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)
}
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.