This repository has been archived on 2023-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
dex/vendor/github.com/russellhaering/goxmldsig/validate.go
2017-03-21 09:27:22 -07:00

411 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)
}