-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC 3161 Timestamping Support #16
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,14 +15,7 @@ | |
package dsse | ||
|
||
import ( | ||
"bytes" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"fmt" | ||
"io" | ||
"time" | ||
|
||
"github.com/testifysec/go-witness/cryptoutil" | ||
) | ||
|
||
type ErrNoSignatures struct{} | ||
|
@@ -61,12 +54,20 @@ type Envelope struct { | |
} | ||
|
||
type Signature struct { | ||
KeyID string `json:"keyid"` | ||
Signature []byte `json:"sig"` | ||
Certificate []byte `json:"certificate,omitempty"` | ||
Intermediates [][]byte `json:"intermediates,omitempty"` | ||
KeyID string `json:"keyid"` | ||
Signature []byte `json:"sig"` | ||
Certificate []byte `json:"certificate,omitempty"` | ||
Intermediates [][]byte `json:"intermediates,omitempty"` | ||
Timestamps []SignatureTimestamp `json:"timestamps,omitempty"` | ||
} | ||
|
||
type SignatureTimestampType string | ||
|
||
const TimestampRFC3161 SignatureTimestampType = "tsp" | ||
|
||
trustedTime time.Time | ||
type SignatureTimestamp struct { | ||
Type SignatureTimestampType `json:"type"` | ||
Data []byte `json:"data"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this data encoded? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Base64. Go json marshals all byte arrays to base64 strings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as what data actually is that is an implementation detail of the timestamper being used. In the case of TSP it’s an ASN1 DER encoded Timestamp Token object. |
||
} | ||
|
||
// preauthEncode wraps the data to be signed or verified and it's type in the DSSE protocol's | ||
|
@@ -76,205 +77,3 @@ func preauthEncode(bodyType string, body []byte) []byte { | |
const dsseVersion = "DSSEv1" | ||
return []byte(fmt.Sprintf("%s %d %s %d %s", dsseVersion, len(bodyType), bodyType, len(body), body)) | ||
} | ||
|
||
// TODO: it'd be nice to break some of this logic out of what should be a presentation layer only | ||
func Sign(bodyType string, body io.Reader, signers ...cryptoutil.Signer) (Envelope, error) { | ||
env := Envelope{} | ||
bodyBytes, err := io.ReadAll(body) | ||
if err != nil { | ||
return env, err | ||
} | ||
|
||
env.PayloadType = bodyType | ||
env.Payload = bodyBytes | ||
env.Signatures = make([]Signature, 0) | ||
pae := preauthEncode(bodyType, bodyBytes) | ||
for _, signer := range signers { | ||
sig, err := signer.Sign(bytes.NewReader(pae)) | ||
if err != nil { | ||
return env, err | ||
} | ||
|
||
keyID, err := signer.KeyID() | ||
if err != nil { | ||
return env, err | ||
} | ||
|
||
dsseSig := Signature{ | ||
KeyID: keyID, | ||
Signature: sig, | ||
} | ||
|
||
if trustBundler, ok := signer.(cryptoutil.TrustBundler); ok { | ||
leaf := trustBundler.Certificate() | ||
intermediates := trustBundler.Intermediates() | ||
if leaf != nil { | ||
dsseSig.Certificate = pem.EncodeToMemory(&pem.Block{Type: PemTypeCertificate, Bytes: leaf.Raw}) | ||
} | ||
|
||
for _, intermediate := range intermediates { | ||
dsseSig.Intermediates = append(dsseSig.Intermediates, pem.EncodeToMemory(&pem.Block{Type: PemTypeCertificate, Bytes: intermediate.Raw})) | ||
} | ||
} | ||
|
||
env.Signatures = append(env.Signatures, dsseSig) | ||
} | ||
|
||
return env, nil | ||
} | ||
|
||
type VerificationOption func(*verificationOptions) | ||
|
||
type verificationOptions struct { | ||
roots []*x509.Certificate | ||
intermediates []*x509.Certificate | ||
verifiers []cryptoutil.Verifier | ||
threshold int | ||
} | ||
|
||
func WithRoots(roots []*x509.Certificate) VerificationOption { | ||
return func(vo *verificationOptions) { | ||
vo.roots = roots | ||
} | ||
} | ||
|
||
func WithIntermediates(intermediates []*x509.Certificate) VerificationOption { | ||
return func(vo *verificationOptions) { | ||
vo.intermediates = intermediates | ||
} | ||
} | ||
|
||
func WithVerifiers(verifiers []cryptoutil.Verifier) VerificationOption { | ||
return func(vo *verificationOptions) { | ||
vo.verifiers = verifiers | ||
} | ||
} | ||
|
||
func WithThreshold(threshold int) VerificationOption { | ||
return func(vo *verificationOptions) { | ||
vo.threshold = threshold | ||
} | ||
} | ||
|
||
func (e Envelope) Verify(opts ...VerificationOption) ([]cryptoutil.Verifier, error) { | ||
options := &verificationOptions{ | ||
threshold: 1, | ||
} | ||
|
||
for _, opt := range opts { | ||
opt(options) | ||
} | ||
|
||
if options.threshold <= 0 { | ||
return nil, ErrInvalidThreshold(options.threshold) | ||
} | ||
|
||
pae := preauthEncode(e.PayloadType, e.Payload) | ||
if len(e.Signatures) == 0 { | ||
return nil, ErrNoSignatures{} | ||
} | ||
|
||
matchingSigFound := false | ||
passedVerifiers := make([]cryptoutil.Verifier, 0) | ||
for _, sig := range e.Signatures { | ||
if sig.Certificate != nil && len(sig.Certificate) > 0 { | ||
cert, err := TryParseCertificate(sig.Certificate) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
sigIntermediates := make([]*x509.Certificate, 0) | ||
for _, int := range sig.Intermediates { | ||
intCert, err := TryParseCertificate(int) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
sigIntermediates = append(sigIntermediates, intCert) | ||
} | ||
|
||
sigIntermediates = append(sigIntermediates, options.intermediates...) | ||
verifier, err := cryptoutil.NewX509Verifier(cert, sigIntermediates, options.roots, sig.trustedTime) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := verifier.Verify(bytes.NewReader(pae), sig.Signature); err == nil { | ||
passedVerifiers = append(passedVerifiers, verifier) | ||
matchingSigFound = true | ||
} | ||
} | ||
|
||
for _, verifier := range options.verifiers { | ||
if verifier != nil { | ||
if err := verifier.Verify(bytes.NewReader(pae), sig.Signature); err == nil { | ||
passedVerifiers = append(passedVerifiers, verifier) | ||
matchingSigFound = true | ||
} | ||
} | ||
} | ||
} | ||
|
||
if !matchingSigFound { | ||
return nil, ErrNoMatchingSigs{} | ||
} | ||
|
||
if len(passedVerifiers) < options.threshold { | ||
return passedVerifiers, ErrThresholdNotMet{Theshold: options.threshold, Acutal: len(passedVerifiers)} | ||
} | ||
|
||
return passedVerifiers, nil | ||
} | ||
|
||
func TryParseCertificate(data []byte) (*x509.Certificate, error) { | ||
possibleCert, err := cryptoutil.TryParseKeyFromReader(bytes.NewReader(data)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
cert, ok := possibleCert.(*x509.Certificate) | ||
if !ok { | ||
return nil, fmt.Errorf("data was a valid verifier but not a certificate") | ||
} | ||
|
||
return cert, nil | ||
} | ||
|
||
type SignatureOption func(so *signatureOptions) | ||
|
||
type signatureOptions struct { | ||
cert []byte | ||
intermediates [][]byte | ||
trustedTime time.Time | ||
} | ||
|
||
func SignatureWithCertificate(certBytes []byte) SignatureOption { | ||
return func(so *signatureOptions) { | ||
so.cert = certBytes | ||
} | ||
} | ||
|
||
func SignatureWithIntermediates(intermediates [][]byte) SignatureOption { | ||
return func(so *signatureOptions) { | ||
so.intermediates = intermediates | ||
} | ||
} | ||
func SignatureWithTrustedTime(trustedTime time.Time) SignatureOption { | ||
return func(so *signatureOptions) { | ||
so.trustedTime = trustedTime | ||
} | ||
} | ||
func NewSignature(keyID string, sig []byte, opts ...SignatureOption) Signature { | ||
so := signatureOptions{} | ||
for _, opt := range opts { | ||
opt(&so) | ||
} | ||
|
||
return Signature{ | ||
KeyID: keyID, | ||
Signature: sig, | ||
Certificate: so.cert, | ||
Intermediates: so.intermediates, | ||
trustedTime: so.trustedTime, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a timestamp signature on the signature, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct