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/signature.go

292 lines
10 KiB

package capsule
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"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
// AlgTest and SealTypeTest are reserved for tests: no version defines
// them, so they are F1 and S1 in every version (§29.3).
AlgTest = 4294967295
SealTypeTest = 4294967295
)
// 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
}
// CMSSigner makes the signature of alg 2 (spec v0.11, §29.10). EncryptFiles
// calls Sign with AUTHOR_MESSAGE once the capsule is prepared, and waits: the
// person signs it outside, with AutoFirma or another application, and Sign
// returns the DER of the CMS signature that she got, with its seals.
type CMSSigner interface {
// Signers returns the SHA-256 of the certificate of each required
// signer, from 1 to 16.
Signers() [][32]byte
// Sign returns the detached CMS signature of message, in DER.
Sign(message []byte) ([]byte, error)
}
// Sealer asks an authority for an RFC 3161 time-stamp token (spec §29.11).
type Sealer interface {
// Seal returns the DER of the token over SHA-256(subject), where subject
// is SEAL_SUBJECT; the imprint SHA-256 of the 32 bytes is the one that
// the token must hold.
Seal(subject [32]byte) ([]byte, error)
}
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)
}
// SecurityKey2 returns the exact content of key 2 of SECURITY_CBOR and, when
// it is an author-signature that decodes, the signature value it holds: what
// a generator of test vectors records about a signature. It fails when
// security has no key 2 or its content does not decode.
func SecurityKey2(security []byte) (content, value []byte, err error) {
w, ok := decodeSecurity(security)
if !ok || w.signature == nil {
return nil, nil, errors.New("capsule: SECURITY_CBOR holds no author-signature")
}
a, err := decodeAuthorSignature(w.signature)
if err != nil {
return nil, nil, err
}
return w.signature, a.value, nil
}
// SecurityKey3 returns the seal_type and the token of key 3 of SECURITY_CBOR:
// what a generator of test vectors records about a seal. It fails when
// security has no key 3 or its content does not decode.
func SecurityKey3(security []byte) (sealType uint64, token []byte, err error) {
w, ok := decodeSecurity(security)
if !ok || w.seal == nil {
return 0, nil, errors.New("capsule: SECURITY_CBOR holds no seal")
}
s, err := decodeSeal(w.seal)
if err != nil {
return 0, nil, err
}
return s.sealType, s.token, nil
}
// DecodeAuthorSignature reads the content of key 2 of SECURITY_CBOR: the alg,
// key 1 (the public key of alg 1, SIGNERS of alg 2) and the signature value.
func DecodeAuthorSignature(content []byte) (alg uint64, key, value []byte, err error) {
a, err := decodeAuthorSignature(content)
if err != nil {
return 0, nil, nil, err
}
return a.alg, a.key, a.value, nil
}
// 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.
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
func EvaluateSecurityIn(b []byte, c *SecurityContext) Verdicts {
// Security never decides the opening (spec §29.3): whatever a parser does
// with hostile input, the capsule opens. A panic is a failure of its own
// part only, as a failure of its form would be: X for the outer map, F1
// for the signature and S2 for the seal, each apart from the other.
var w *securityWire
if !recovered(func() { w, _ = decodeSecurity(b) }) || w == nil {
return Verdicts{Signature: VerdictUnreadable, Seal: VerdictUnreadable}
}
v := Verdicts{Signature: VerdictNoSignature, Seal: VerdictNoSeal}
if w.signature != nil {
v.Signature = VerdictSignatureUnchecked
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
if c != nil && !recovered(func() { evaluateSignature(&v, w, c) }) {
v = Verdicts{Signature: VerdictSignatureUnchecked, Seal: VerdictNoSeal}
}
}
if w.seal != nil {
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
signature := v
if !recovered(func() { setSeal(&v, w, c) }) {
v = signature
v.Seal = VerdictSealUnreadable
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
if v.Detail != nil {
d := *v.Detail
d.SealHolder, d.SealTime = "", time.Time{}
v.Detail = &d
}
}
}
return v
}
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
// setSeal sets the verdict of the seal of w (spec §29.7): S2 for content that
// breaks the schema of seal, S1 for a seal_type this reader does not
// implement, and the verdicts of §29.11 for seal_type 2.
func setSeal(v *Verdicts, w *securityWire, c *SecurityContext) {
s, err := decodeSeal(w.seal)
switch {
case err != nil:
v.Seal = VerdictSealUnreadable
case s.sealType == SealTypeRFC3161 && c != nil:
evaluateSeal(v, s, w.signature, c)
default:
v.Seal = VerdictSealUnsupported
}
}
// recovered runs f and reports whether it returned without a panic.
func recovered(f func()) (ok bool) {
defer func() {
if recover() != nil {
ok = false
}
}()
f()
return true
}
// 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, w *securityWire, c *SecurityContext) {
a, err := decodeAuthorSignature(w.signature)
if err != nil {
return
}
if a.alg == AlgCMS {
evaluateCMS(v, a, w.seal != nil, c)
return
}
if 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
}
}
}

Powered by TurnKey Linux.