forked from mystiq/dex
410 lines
12 KiB
Go
410 lines
12 KiB
Go
package dsig
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"github.com/beevik/etree"
|
|
"github.com/russellhaering/goxmldsig/etreeutils"
|
|
)
|
|
|
|
var uriRegexp = regexp.MustCompile("^#[a-zA-Z_][\\w.-]*$")
|
|
|
|
var (
|
|
// ErrMissingSignature indicates that no enveloped signature was found referencing
|
|
// the top level element passed for signature verification.
|
|
ErrMissingSignature = errors.New("Missing signature referencing the top-level element")
|
|
)
|
|
|
|
type ValidationContext struct {
|
|
CertificateStore X509CertificateStore
|
|
IdAttribute string
|
|
Clock *Clock
|
|
}
|
|
|
|
func NewDefaultValidationContext(certificateStore X509CertificateStore) *ValidationContext {
|
|
return &ValidationContext{
|
|
CertificateStore: certificateStore,
|
|
IdAttribute: DefaultIdAttr,
|
|
}
|
|
}
|
|
|
|
// TODO(russell_h): More flexible namespace support. This might barely work.
|
|
func inNamespace(el *etree.Element, ns string) bool {
|
|
for _, attr := range el.Attr {
|
|
if attr.Value == ns {
|
|
if attr.Space == "" && attr.Key == "xmlns" {
|
|
return el.Space == ""
|
|
} else if attr.Space == "xmlns" {
|
|
return el.Space == attr.Key
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func childPath(space, tag string) string {
|
|
if space == "" {
|
|
return "./" + tag
|
|
} else {
|
|
return "./" + space + ":" + tag
|
|
}
|
|
}
|
|
|
|
// The RemoveElement method on etree.Element isn't recursive...
|
|
func recursivelyRemoveElement(tree, el *etree.Element) bool {
|
|
if tree.RemoveChild(el) != nil {
|
|
return true
|
|
}
|
|
|
|
for _, child := range tree.Child {
|
|
if childElement, ok := child.(*etree.Element); ok {
|
|
if recursivelyRemoveElement(childElement, el) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// transform applies the passed set of transforms to the specified root element.
|
|
//
|
|
// The functionality of transform is currently very limited and purpose-specific.
|
|
//
|
|
// NOTE(russell_h): Ideally this wouldn't mutate the root passed to it, and would
|
|
// instead return a copy. Unfortunately copying the tree makes it difficult to
|
|
// correctly locate the signature. I'm opting, for now, to simply mutate the root
|
|
// parameter.
|
|
func (ctx *ValidationContext) transform(root, sig *etree.Element, transforms []*etree.Element) (*etree.Element, Canonicalizer, error) {
|
|
if len(transforms) != 2 {
|
|
return nil, nil, errors.New("Expected Enveloped and C14N transforms")
|
|
}
|
|
|
|
var canonicalizer Canonicalizer
|
|
|
|
for _, transform := range transforms {
|
|
algo := transform.SelectAttr(AlgorithmAttr)
|
|
if algo == nil {
|
|
return nil, nil, errors.New("Missing Algorithm attribute")
|
|
}
|
|
|
|
switch AlgorithmID(algo.Value) {
|
|
case EnvelopedSignatureAltorithmId:
|
|
if !recursivelyRemoveElement(root, sig) {
|
|
return nil, nil, errors.New("Error applying canonicalization transform: Signature not found")
|
|
}
|
|
|
|
case CanonicalXML10ExclusiveAlgorithmId:
|
|
var prefixList string
|
|
ins := transform.FindElement(childPath("", InclusiveNamespacesTag))
|
|
if ins != nil {
|
|
prefixListEl := ins.SelectAttr(PrefixListAttr)
|
|
if prefixListEl != nil {
|
|
prefixList = prefixListEl.Value
|
|
}
|
|
}
|
|
|
|
canonicalizer = MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList)
|
|
|
|
case CanonicalXML11AlgorithmId:
|
|
canonicalizer = MakeC14N11Canonicalizer()
|
|
|
|
default:
|
|
return nil, nil, errors.New("Unknown Transform Algorithm: " + algo.Value)
|
|
}
|
|
}
|
|
|
|
if canonicalizer == nil {
|
|
return nil, nil, errors.New("Expected canonicalization transform")
|
|
}
|
|
|
|
return root, canonicalizer, nil
|
|
}
|
|
|
|
func (ctx *ValidationContext) digest(el *etree.Element, digestAlgorithmId string, canonicalizer Canonicalizer) ([]byte, error) {
|
|
data, err := canonicalizer.Canonicalize(el)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
digestAlgorithm, ok := digestAlgorithmsByIdentifier[digestAlgorithmId]
|
|
if !ok {
|
|
return nil, errors.New("Unknown digest algorithm: " + digestAlgorithmId)
|
|
}
|
|
|
|
hash := digestAlgorithm.New()
|
|
_, err = hash.Write(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return hash.Sum(nil), nil
|
|
}
|
|
|
|
func (ctx *ValidationContext) verifySignedInfo(signatureElement *etree.Element, canonicalizer Canonicalizer, signatureMethodId string, cert *x509.Certificate, sig []byte) error {
|
|
signedInfo := signatureElement.FindElement(childPath(signatureElement.Space, SignedInfoTag))
|
|
if signedInfo == nil {
|
|
return errors.New("Missing SignedInfo")
|
|
}
|
|
|
|
// Any attributes from the 'Signature' element must be pushed down into the 'SignedInfo' element before it is canonicalized
|
|
for _, attr := range signatureElement.Attr {
|
|
signedInfo.CreateAttr(attr.Space+":"+attr.Key, attr.Value)
|
|
}
|
|
|
|
// Canonicalize the xml
|
|
canonical, err := canonicalizer.Canonicalize(signedInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
signatureAlgorithm, ok := signatureMethodsByIdentifier[signatureMethodId]
|
|
if !ok {
|
|
return errors.New("Unknown signature method: " + signatureMethodId)
|
|
}
|
|
|
|
hash := signatureAlgorithm.New()
|
|
_, err = hash.Write(canonical)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hashed := hash.Sum(nil)
|
|
|
|
pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
|
|
if !ok {
|
|
return errors.New("Invalid public key")
|
|
}
|
|
|
|
// Verify that the private key matching the public key from the cert was what was used to sign the 'SignedInfo' and produce the 'SignatureValue'
|
|
err = rsa.VerifyPKCS1v15(pubKey, signatureAlgorithm, hashed[:], sig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *ValidationContext) validateSignature(el, sig *etree.Element, cert *x509.Certificate) (*etree.Element, error) {
|
|
// Get the 'SignedInfo' element
|
|
signedInfo := sig.FindElement(childPath(sig.Space, SignedInfoTag))
|
|
if signedInfo == nil {
|
|
return nil, errors.New("Missing SignedInfo")
|
|
}
|
|
|
|
reference := signedInfo.FindElement(childPath(sig.Space, ReferenceTag))
|
|
if reference == nil {
|
|
return nil, errors.New("Missing Reference")
|
|
}
|
|
|
|
transforms := reference.FindElement(childPath(sig.Space, TransformsTag))
|
|
if transforms == nil {
|
|
return nil, errors.New("Missing Transforms")
|
|
}
|
|
|
|
uri := reference.SelectAttr("URI")
|
|
if uri == nil {
|
|
// TODO(russell_h): It is permissible to leave this out. We should be
|
|
// able to fall back to finding the referenced element some other way.
|
|
return nil, errors.New("Reference is missing URI attribute")
|
|
}
|
|
|
|
// Get the element referenced in the 'SignedInfo'
|
|
referencedElement := el.FindElement(fmt.Sprintf("//[@%s='%s']", ctx.IdAttribute, uri.Value[1:]))
|
|
if referencedElement == nil {
|
|
return nil, errors.New("Unable to find referenced element: " + uri.Value)
|
|
}
|
|
|
|
// Perform all transformations listed in the 'SignedInfo'
|
|
// Basically, this means removing the 'SignedInfo'
|
|
transformed, canonicalizer, err := ctx.transform(referencedElement, sig, transforms.ChildElements())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
digestMethod := reference.FindElement(childPath(sig.Space, DigestMethodTag))
|
|
if digestMethod == nil {
|
|
return nil, errors.New("Missing DigestMethod")
|
|
}
|
|
|
|
digestValue := reference.FindElement(childPath(sig.Space, DigestValueTag))
|
|
if digestValue == nil {
|
|
return nil, errors.New("Missing DigestValue")
|
|
}
|
|
|
|
digestAlgorithmAttr := digestMethod.SelectAttr(AlgorithmAttr)
|
|
if digestAlgorithmAttr == nil {
|
|
return nil, errors.New("Missing DigestMethod Algorithm attribute")
|
|
}
|
|
|
|
// Digest the transformed XML and compare it to the 'DigestValue' from the 'SignedInfo'
|
|
digest, err := ctx.digest(transformed, digestAlgorithmAttr.Value, canonicalizer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decodedDigestValue, err := base64.StdEncoding.DecodeString(digestValue.Text())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !bytes.Equal(digest, decodedDigestValue) {
|
|
return nil, errors.New("Signature could not be verified")
|
|
}
|
|
|
|
//Verify the signed info
|
|
signatureMethod := signedInfo.FindElement(childPath(sig.Space, SignatureMethodTag))
|
|
if signatureMethod == nil {
|
|
return nil, errors.New("Missing SignatureMethod")
|
|
}
|
|
|
|
signatureMethodAlgorithmAttr := signatureMethod.SelectAttr(AlgorithmAttr)
|
|
if digestAlgorithmAttr == nil {
|
|
return nil, errors.New("Missing SignatureMethod Algorithm attribute")
|
|
}
|
|
|
|
// Decode the 'SignatureValue' so we can compare against it
|
|
signatureValue := sig.FindElement(childPath(sig.Space, SignatureValueTag))
|
|
if signatureValue == nil {
|
|
return nil, errors.New("Missing SignatureValue")
|
|
}
|
|
|
|
decodedSignature, err := base64.StdEncoding.DecodeString(signatureValue.Text())
|
|
|
|
if err != nil {
|
|
return nil, errors.New("Could not decode signature")
|
|
}
|
|
// Actually verify the 'SignedInfo' was signed by a trusted source
|
|
err = ctx.verifySignedInfo(sig, canonicalizer, signatureMethodAlgorithmAttr.Value, cert, decodedSignature)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transformed, nil
|
|
}
|
|
|
|
func contains(roots []*x509.Certificate, cert *x509.Certificate) bool {
|
|
for _, root := range roots {
|
|
if root.Equal(cert) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// findSignature searches for a Signature element referencing the passed root element.
|
|
func (ctx *ValidationContext) findSignature(el *etree.Element) (*etree.Element, error) {
|
|
idAttr := el.SelectAttr(DefaultIdAttr)
|
|
if idAttr == nil || idAttr.Value == "" {
|
|
return nil, errors.New("Missing ID attribute")
|
|
}
|
|
|
|
var signatureElement *etree.Element
|
|
|
|
err := etreeutils.NSFindIterate(el, Namespace, SignatureTag, func(sig *etree.Element) error {
|
|
signedInfo := sig.FindElement(childPath(sig.Space, SignedInfoTag))
|
|
if signedInfo == nil {
|
|
return errors.New("Missing SignedInfo")
|
|
}
|
|
|
|
referenceElement := signedInfo.FindElement(childPath(sig.Space, ReferenceTag))
|
|
if referenceElement == nil {
|
|
return errors.New("Missing Reference Element")
|
|
}
|
|
|
|
uriAttr := referenceElement.SelectAttr(URIAttr)
|
|
if uriAttr == nil || uriAttr.Value == "" {
|
|
return errors.New("Missing URI attribute")
|
|
}
|
|
|
|
if !uriRegexp.MatchString(uriAttr.Value) {
|
|
return errors.New("Invalid URI: " + uriAttr.Value)
|
|
}
|
|
|
|
if uriAttr.Value[1:] == idAttr.Value {
|
|
signatureElement = sig
|
|
return etreeutils.ErrTraversalHalted
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if signatureElement == nil {
|
|
return nil, ErrMissingSignature
|
|
}
|
|
|
|
return signatureElement, nil
|
|
}
|
|
|
|
func (ctx *ValidationContext) verifyCertificate(signatureElement *etree.Element) (*x509.Certificate, error) {
|
|
now := ctx.Clock.Now()
|
|
|
|
roots, err := ctx.CertificateStore.Certificates()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cert *x509.Certificate
|
|
|
|
// Get the x509 element from the signature
|
|
x509Element := signatureElement.FindElement("//" + childPath(signatureElement.Space, X509CertificateTag))
|
|
if x509Element == nil {
|
|
// Use root certificate if there is only one and it is not contained in signatureElement
|
|
if len(roots) == 1 {
|
|
cert = roots[0]
|
|
} else {
|
|
return nil, errors.New("Missing x509 Element")
|
|
}
|
|
} else {
|
|
certData, err := base64.StdEncoding.DecodeString(x509Element.Text())
|
|
if err != nil {
|
|
return nil, errors.New("Failed to parse certificate")
|
|
}
|
|
|
|
cert, err = x509.ParseCertificate(certData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Verify that the certificate is one we trust
|
|
if !contains(roots, cert) {
|
|
return nil, errors.New("Could not verify certificate against trusted certs")
|
|
}
|
|
|
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
|
return nil, errors.New("Cert is not valid at this time")
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// Validate verifies that the passed element contains a valid enveloped signature
|
|
// matching a currently-valid certificate in the context's CertificateStore.
|
|
func (ctx *ValidationContext) Validate(el *etree.Element) (*etree.Element, error) {
|
|
// Make a copy of the element to avoid mutating the one we were passed.
|
|
el = el.Copy()
|
|
|
|
sig, err := ctx.findSignature(el)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cert, err := ctx.verifyCertificate(sig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ctx.validateSignature(el, sig, cert)
|
|
}
|