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.
230 lines
6.8 KiB
230 lines
6.8 KiB
package capsule
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/internal/cms"
|
|
"g.activething.com/go/DateKeys/internal/pathrule"
|
|
)
|
|
|
|
// SealTypeRFC3161 is the seal_type of a seal that is an RFC 3161 time-stamp
|
|
// token (spec v0.11, §29.3, §29.11).
|
|
const SealTypeRFC3161 = 2
|
|
|
|
// MaxSigners is the most required signers of an alg 2 signature (§29.10).
|
|
const MaxSigners = 16
|
|
|
|
// EncodeSigners returns SIGNERS, the content of key 1 of an author-signature
|
|
// of alg 2: a CBOR array of 1 to 16 strings of 32 bytes, the SHA-256 of the
|
|
// certificate of each required signer, in strictly ascending order of bytes
|
|
// (spec §29.10). It sorts them, and fails when there are none, too many or
|
|
// two are equal.
|
|
func EncodeSigners(hashes [][32]byte) ([]byte, error) {
|
|
if len(hashes) < 1 || len(hashes) > MaxSigners {
|
|
return nil, errors.New("capsule: SIGNERS holds from 1 to 16 certificates")
|
|
}
|
|
sorted := append([][32]byte(nil), hashes...)
|
|
for i := 1; i < len(sorted); i++ { // insertion sort: at most 16
|
|
for j := i; j > 0 && bytes.Compare(sorted[j-1][:], sorted[j][:]) > 0; j-- {
|
|
sorted[j-1], sorted[j] = sorted[j], sorted[j-1]
|
|
}
|
|
}
|
|
for i := 1; i < len(sorted); i++ {
|
|
if sorted[i-1] == sorted[i] {
|
|
return nil, errors.New("capsule: SIGNERS names a certificate twice")
|
|
}
|
|
}
|
|
var e codec.Encoder
|
|
e.Array(len(sorted))
|
|
for _, h := range sorted {
|
|
e.Bstr(h[:])
|
|
}
|
|
return e.Out()
|
|
}
|
|
|
|
// decodeSigners reads SIGNERS and checks the profile of spec §29.10.
|
|
func decodeSigners(b []byte) ([][32]byte, error) {
|
|
var out [][32]byte
|
|
decode := func(d *codec.Decoder) error {
|
|
n, err := d.Array(MaxSigners)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n < 1 {
|
|
return errors.New("SIGNERS is empty")
|
|
}
|
|
for range n {
|
|
h, err := d.Bstr(32, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var x [32]byte
|
|
copy(x[:], h)
|
|
if len(out) > 0 && bytes.Compare(out[len(out)-1][:], x[:]) >= 0 {
|
|
return errors.New("SIGNERS is not in strictly ascending order")
|
|
}
|
|
out = append(out, x)
|
|
}
|
|
return nil
|
|
}
|
|
encode := func(e *codec.Encoder) {
|
|
e.Array(len(out))
|
|
for _, h := range out {
|
|
e.Bstr(h[:])
|
|
}
|
|
}
|
|
if err := codec.Unmarshal(b, decode, encode); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// MaxNameLen is the most code points of a name of a certificate that §29.7
|
|
// shows, the upper bound of a commonName in X.520.
|
|
const MaxNameLen = 64
|
|
|
|
// holderText is how §29.7 shows a name of a certificate: the name, when it
|
|
// meets the rules of the declared author, has at most MaxNameLen code points
|
|
// and no two spaces in a row, and the SHA-256 given otherwise. A name cannot
|
|
// then line up, with spaces, a text of its own where a terminal breaks the
|
|
// line.
|
|
func holderText(name string, hash [32]byte) string {
|
|
if name != "" && utf8.ValidString(name) && utf8.RuneCountInString(name) <= MaxNameLen && !strings.Contains(name, " ") && pathrule.CheckAuthor(name) == nil {
|
|
return name
|
|
}
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// evaluateCMS sets the verdict of a signature of alg 2 (spec §29.10): F1 for
|
|
// content that breaks its profile, F2 when the signature of a required
|
|
// signer is invalid, F5 when something the capsule demands is missing, F6
|
|
// when every required signer is valid and sealed. hasSeal is whether key 3
|
|
// exists, which an alg 2 signature forbids.
|
|
func evaluateCMS(v *Verdicts, a *authorSignature, hasSeal bool, c *SecurityContext) {
|
|
required, err := decodeSigners(a.key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
sd, err := cms.ParseSignature(a.value)
|
|
if err != nil {
|
|
return
|
|
}
|
|
msg := AuthorMessage(c.ControlCommit, c.HeadDigest, SignersDigest(AlgCMS, a.key))
|
|
detail := &Detail{}
|
|
byHash := map[[32]byte]*cms.SignerInfo{}
|
|
for _, s := range sd.Signers {
|
|
byHash[s.Cert.Hash] = s
|
|
}
|
|
invalid, incomplete := false, hasSeal
|
|
for _, h := range required {
|
|
s := byHash[h]
|
|
if s == nil {
|
|
detail.Signers = append(detail.Signers, SignerLine{Holder: hex.EncodeToString(h[:]), Result: "absent"})
|
|
incomplete = true
|
|
continue
|
|
}
|
|
line := signerLine(s, msg, c.RoundTime)
|
|
switch line.Result {
|
|
case "invalid":
|
|
invalid = true
|
|
case "valid":
|
|
default:
|
|
incomplete = true
|
|
}
|
|
detail.Signers = append(detail.Signers, line)
|
|
}
|
|
for _, s := range sd.Signers {
|
|
if !isRequired(required, s.Cert.Hash) {
|
|
detail.Foreign = append(detail.Foreign, signerLine(s, msg, c.RoundTime))
|
|
}
|
|
}
|
|
v.Detail = detail
|
|
switch {
|
|
case invalid:
|
|
v.Signature = VerdictSignatureInvalid
|
|
case incomplete:
|
|
v.Signature = VerdictSignedIncomplete
|
|
default:
|
|
v.Signature = VerdictSignedComplete
|
|
}
|
|
}
|
|
|
|
func isRequired(required [][32]byte, h [32]byte) bool {
|
|
for _, r := range required {
|
|
if r == h {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// signerLine checks one SignerInfo as §29.10 orders: not verifiable, invalid,
|
|
// without seal, with an invalid seal, out of validity, or valid.
|
|
func signerLine(s *cms.SignerInfo, msg []byte, roundTime time.Time) SignerLine {
|
|
// The issuer is text of the certificate, as the holder is: it gets the same
|
|
// rules, and the SHA-256 of the issuer when it fails them, so that no
|
|
// escape, no control and no bidi character reaches a line of the verdicts.
|
|
l := SignerLine{Holder: holderText(s.Cert.Holder(), s.Cert.Hash), Issuer: holderText(s.Cert.IssuerName(), sha256.Sum256(s.Cert.RawIssuer))}
|
|
switch s.Check(msg) {
|
|
case cms.NotVerifiable:
|
|
l.Result = "not verifiable"
|
|
return l
|
|
case cms.Invalid:
|
|
l.Result = "invalid"
|
|
return l
|
|
}
|
|
if s.Token == nil {
|
|
l.Result = "without seal"
|
|
return l
|
|
}
|
|
tok, err := cms.ParseToken(s.Token)
|
|
if err != nil || !tok.Check(s.Signature) {
|
|
l.Result = "invalid seal"
|
|
return l
|
|
}
|
|
if !s.Cert.ValidAt(tok.GenTime) {
|
|
l.Result = "out of validity"
|
|
return l
|
|
}
|
|
l.Result, l.SealTime, l.SealHolder = "valid", tok.GenTime, holderText(tok.TSA.Holder(), tok.TSA.Hash)
|
|
l.Reason = sealReason(tok, roundTime)
|
|
l.Before = l.Reason == ReasonNone
|
|
return l
|
|
}
|
|
|
|
// evaluateSeal sets the verdict of a seal of seal_type 2 (spec §29.11): S2 or
|
|
// S1 for the form and the algorithms, S3 when it does not verify, and S4 or
|
|
// S5 when it does. signature is the content of key 2, nil without it.
|
|
func evaluateSeal(v *Verdicts, s *seal, signature []byte, c *SecurityContext) {
|
|
tok, err := cms.ParseToken(s.token)
|
|
switch {
|
|
case errors.Is(err, cms.ErrForm):
|
|
v.Seal = VerdictSealUnreadable
|
|
return
|
|
case err != nil || !tok.ImprintIsSHA256():
|
|
v.Seal = VerdictSealUnsupported
|
|
return
|
|
}
|
|
subject := SealSubject(c.ControlCommit, c.HeadDigest, SigPart(signature))
|
|
if !tok.Check(subject[:]) {
|
|
v.Seal = VerdictSealInvalid
|
|
return
|
|
}
|
|
if v.Detail == nil {
|
|
v.Detail = &Detail{}
|
|
}
|
|
v.Detail.SealHolder, v.Detail.SealTime = holderText(tok.TSA.Holder(), tok.TSA.Hash), tok.GenTime
|
|
v.Detail.SealReason = sealReason(tok, c.RoundTime)
|
|
v.Seal = VerdictSealedLate
|
|
if v.Detail.SealReason == ReasonNone {
|
|
v.Seal = VerdictSealed
|
|
}
|
|
}
|