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.
292 lines
10 KiB
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.
|
|
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
|
|
if c != nil && !recovered(func() { evaluateSignature(&v, w, c) }) {
|
|
v = Verdicts{Signature: VerdictSignatureUnchecked, Seal: VerdictNoSeal}
|
|
}
|
|
}
|
|
if w.seal != nil {
|
|
signature := v
|
|
if !recovered(func() { setSeal(&v, w, c) }) {
|
|
v = signature
|
|
v.Seal = VerdictSealUnreadable
|
|
if v.Detail != nil {
|
|
d := *v.Detail
|
|
d.SealHolder, d.SealTime = "", time.Time{}
|
|
v.Detail = &d
|
|
}
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|