The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/* zxdecode.c  -  SAML Decoding tool
 * Copyright (c) 2012 Synergetics SA (sampo@synergetics.be), All Rights Reserved.
 * Copyright (c) 2008-2011 Sampo Kellomaki (sampo@iki.fi), All Rights Reserved.
 * This is confidential unpublished proprietary source code of the author.
 * NO WARRANTY, not even implied warranties. Contains trade secrets.
 * Distribution prohibited unless authorized in writing.
 * Licensed under Apache License 2.0, see file COPYING.
 * $Id: zxdecode.c,v 1.8 2009-11-29 12:23:06 sampo Exp $
 *
 * 25.11.2008, created --Sampo
 * 4.10.2010, added -s and ss modes, as well as -i N selector --Sampo
 * 25.1.2011, added -wsc and -wsp validation options --Sampo
 * 7.2.2012,  improved decoding encrypted SAML responses --Sampo
 */

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include "platform.h"
#include "errmac.h"
#include "zx.h"
#include "zxid.h"
#include "zxidpriv.h"
#include "zxidutil.h"
#include "zxidconf.h"
#include "c/zxidvers.h"
#include "c/zx-ns.h"
#include "c/zx-const.h"
#include "c/zx-data.h"

char* help =
"zxdecode  -  Decode SAML Redirect and POST Messages R" ZXID_REL "\n\
Copyright (c) 2012 Synergetics SA (sampo@synergetics.be), All Rights Reserved.\n\
Copyright (c) 2008-2011 Sampo Kellomaki (sampo@iki.fi), All Rights Reserved.\n\
NO WARRANTY, not even implied warranties. Licensed under Apache License v2.0\n\
See http://www.apache.org/licenses/LICENSE-2.0\n\
Send well researched bug reports to the author. Home: zxid.org\n\
\n\
Usage: zxdecode [options] <message >decoded\n\
  -b -B            Prevent or force decode base64 step (default auto detects)\n\
  -z -Z            Prevent or force inflate step (default auto detects)\n\
  -i N             Pick Nth detected decodable structure, default: 1=first\n\
  -s               Enable signature validation step (reads config from -c, see below)\n\
  -s -s            Only validate hashes (check canon), do not fetch meta or check RSA\n\
  -r               Decode and validate already decoded SAML2 reponse, e.g. from audit trail\n\
  -c CONF          For -s, optional configuration string (default -c PATH=/var/zxid/)\n\
                   Most of the configuration is read from " ZXID_CONF_PATH "\n\
  -wscp            Call zxid_wsc_prepare_call() on SOAP request\n\
  -wspv            Call zxid_wsp_validate() on SOAP request\n\
  -wspd            Call zxid_wsp_decorate() on SOAP response\n\
  -wscv            Call zxid_wsc_valid_resp() on SOAP response\n\
  -sha1            Compute sha1 over input and print as base64. For debugging canon.\n\
  -v               Verbose messages.\n\
  -q               Be extra quiet.\n\
  -d               Turn on debugging.\n\
  -h               This help message\n\
  --               End of options\n\
\n\
Will attempt to detect many layers of encoding. Will hunt for the\n\
relevant input such as SAMLRequest or SAMLResponse in, e.g., log file.\n";

int b64_flag = 2;      /* Auto */
int inflate_flag = 2;  /* Auto */
int verbose = 1;
int ix = 1;
int sig_flag = 0;  /* No sig checking by default. */
int sha1_flag = 0;
int resp_flag = 0;
char valid_opt = 0;
zxid_conf* cf = 0;
char buf[256*1024];

/* Called by:  main x8, zxbusd_main, zxbuslist_main, zxbustailf_main, zxcall_main, zxcot_main, zxdecode_main */
static void opt(int* argc, char*** argv, char*** env)
{
  if (*argc <= 1) return;
  
  while (1) {
    ++(*argv); --(*argc);
    
    if (!(*argc) || ((*argv)[0][0] != '-')) break;  /* normal exit from options loop */
    
    switch ((*argv)[0][1]) {
    case '-': if ((*argv)[0][2]) break;
      ++(*argv); --(*argc);
      DD("End of options by --");
      return;  /* -- ends the options */

    case 'c':
      switch ((*argv)[0][2]) {
      case '\0':
	++(*argv); --(*argc);
	if ((*argc) < 1) break;
	if (!cf)
	  cf = zxid_new_conf_to_cf(0);
	zxid_parse_conf(cf, (*argv)[0]);
	continue;
      }
      break;

    case 'd':
      switch ((*argv)[0][2]) {
      case '\0':
	++errmac_debug;
	continue;
      }
      break;

    case 'i':
      switch ((*argv)[0][2]) {
      case '\0':
	++(*argv); --(*argc);
	if ((*argc) < 1) break;
	sscanf((*argv)[0], "%i", &ix);
	continue;
      }
      break;

    case 's':
      switch ((*argv)[0][2]) {
      case '\0':
	++sig_flag;
	if (!cf)
	  cf = zxid_new_conf_to_cf(0);
	continue;
      case 'h':
	++sha1_flag;
	continue;
      }
      break;

    case 'r':
      switch ((*argv)[0][2]) {
      case '\0':
	++resp_flag;
	continue;
      }
      break;

    case 'b':
      switch ((*argv)[0][2]) {
      case '\0':
	b64_flag = 0;
	continue;
      }
      break;
    case 'B':
      switch ((*argv)[0][2]) {
      case '\0':
	b64_flag = 1;
	continue;
      }
      break;

    case 'z':
      switch ((*argv)[0][2]) {
      case '\0':
	inflate_flag = 0;
	continue;
      }
      break;
    case 'Z':
      switch ((*argv)[0][2]) {
      case '\0':
	inflate_flag = 1;
	continue;
      }
      break;

    case 'w':
      switch ((*argv)[0][2]) {
      case 's':
	switch ((*argv)[0][3]) {
	case 'c':
	  switch ((*argv)[0][4]) {
	  case 'p':
	    valid_opt = 'P';
	    continue;
	  case 'v':
	    valid_opt = 'V';
	    continue;
	  }
	  break;
	case 'p':
	  switch ((*argv)[0][4]) {
	  case 'v':
	    valid_opt = 'v';
	    continue;
	  case 'd':
	    valid_opt = 'd';
	    continue;
	  }
	  break;
	}
	break;
      }
      break;

#if 0
    case 'l':
      switch ((*argv)[0][2]) {
      case 'i':
	if (!strcmp((*argv)[0],"-license")) {
	  extern char* license;
	  fprintf(stderr, license);
	  exit(0);
	}
	break;
      }
      break;
#endif

    case 'q':
      switch ((*argv)[0][2]) {
      case '\0':
	verbose = 0;
	continue;
      }
      break;

    case 'v':
      switch ((*argv)[0][2]) {
      case '\0':
	++verbose;
	continue;
      }
      break;

    } 
    /* fall thru means unrecognized flag */
    if (*argc)
      fprintf(stderr, "Unrecognized flag `%s'\n", (*argv)[0]);
    if (verbose>1) {
      printf("%s", help);
      exit(0);
    }
    fprintf(stderr, "%s", help);
    /*fprintf(stderr, "version=0x%06x rel(%s)\n", zxid_version(), zxid_version_str());*/
    exit(3);
  }
}

/* Called by:  zxdecode_main */
static int ws_validations()
{
  int ret;
  char* nid;
  struct zx_str* ss;
  zxid_ses sess;
  ZERO(&sess, sizeof(sess));
  if (!cf)
    cf = zxid_new_conf_to_cf(0);

  switch (valid_opt) {
  case 'P':
    ss = zxid_wsc_prepare_call(cf, &sess, 0 /*epr*/, "", buf);
    if (!ss)
      return 1;
    if (verbose)
      printf("WSC_PREPARE_CALL(%.*s)\n", ss->len, ss->s);
    return 0;
  case 'v':
    nid = zxid_wsp_validate(cf, &sess, "", buf);
    if (!nid)
      return 1;
    if (verbose)
      printf("WSP_VALIDATE OK nid(%s)\n", nid);
    return 0;
  case 'd':
    ss = zxid_wsp_decorate(cf, &sess, "", buf);
    if (!ss)
      return 1;
    if (verbose)
      printf("WSP_DECORATE(%.*s)\n", ss->len, ss->s);
    return 0;
  case 'V':
    ret = zxid_wsc_valid_resp(cf, &sess, "", buf);
    if (verbose)
      printf("WSC_VALID_RESP(%d)\n", ret);
    if (ret == 1)
      return 0; /* Success */
    return 1;
  }
  return 2;
}

/* Called by:  sig_validate */
static int wsse_sec_validate(struct zx_e_Envelope_s* env)
{
  int ret;
  int n_refs = 0;
  struct zxsig_ref refs[ZXID_N_WSF_SIGNED_HEADERS];
  struct zx_wsse_Security_s* sec = env->Header->Security;

  if (!sec || !sec->Signature) {
    ERR("Missing signature on <wsse:Security> %p", sec);
    return 8;
  }
  if (!sec->Signature->SignedInfo || !sec->Signature->SignedInfo->Reference) {
    ERR("Malformed signature, missing mandatory SignedInfo(%p) or Reference", sec->Signature->SignedInfo);
    return 9;
  }
  
  ZERO(refs, sizeof(refs));
  n_refs = zxid_hunt_sig_parts(cf, n_refs, refs, sec->Signature->SignedInfo->Reference, env->Header, env->Body);
  /*zx_see_elem_ns(cf->ctx, &refs.pop_seen, &resp->gg); *** */
  ret = zxsig_validate(cf->ctx, 0, sec->Signature, n_refs, refs);
  if (ret == ZXSIG_BAD_CERT) {
    INFO("Canon sha1 of <wsse:Security> verified OK %d", ret);
    if (verbose)
      printf("\nCanon sha1 if <wsse:Security> verified OK %d\n", ret);
  } else {
    ERR("Response Signature hash validation error. Bad canonicalization? ret=%d",ret);
    return 10;
  }

  if (ret && verbose)
    printf("\nSIG Verified OK, zxid_sp_sso_finalize() returned %d\n", ret);
  return ret?0:6;
}

/*() Process SAML2 response */

/* Called by:  decode, zxdecode_main */
static int sig_validate(int len, char* p)
{
  int ret;
  zxid_cgi cgi;
  zxid_ses ses;
  struct zx_root_s* r;
  struct zx_sp_Response_s* resp;
  struct zx_ns_s* pop_seen = 0;
  struct zxsig_ref refs;
  zxid_a7n* a7n;
  
  ZERO(&cgi, sizeof(cgi));
  ZERO(&ses, sizeof(ses));

  r = zx_dec_zx_root(cf->ctx, len, p, "decode");
  if (!r) {
    ERR("Failed to parse buf(%.*s)", len, p);
    return 2;
  }

  if (r->Response)
    resp = r->Response;  /* Normal SAML2 Response, e.g. from POST */
  else if (r->Envelope && r->Envelope->Body) {
    if (r->Envelope->Body->Response)
      resp = r->Envelope->Body->Response;
    else if (r->Envelope->Body->ArtifactResponse && r->Envelope->Body->ArtifactResponse->Response)
      resp = r->Envelope->Body->ArtifactResponse->Response;
    else if (r->Envelope->Header && r->Envelope->Header->Security)
      return wsse_sec_validate(r->Envelope);
    else {
      ERR("<e:Envelope> found, but no <sp:Response> element in it %d",0);
      return 3;
    }
  } else {
    a7n = zxid_dec_a7n(cf, r->Assertion, r->EncryptedAssertion);
    if (a7n) {
      INFO("Bare Assertion without Response wrapper detected %p", r->Assertion);
      goto got_a7n;
    }
    ERR("No <sp:Response>, <sa:Assertion>, or <sa:EncryptedAssertion> found buf(%.*s)", len, p);
    return 3;
  }

  /* See zxid_sp_dig_sso_a7n() for similar code. */
  
  if (sig_flag == 2) {
    if (!resp->Signature) {
      INFO("No signature in Response %d", 0);
    } else {
      if (!resp->Signature->SignedInfo || !resp->Signature->SignedInfo->Reference) {
	ERR("Malformed signature, missing mandatory SignedInfo(%p) or Reference", resp->Signature->SignedInfo);
	return 9;
      }

      ZERO(&refs, sizeof(refs));
      refs.sref = resp->Signature->SignedInfo->Reference;
      refs.blob = &resp->gg;
      refs.pop_seen = pop_seen;
      zx_see_elem_ns(cf->ctx, &refs.pop_seen, &resp->gg);
      ret = zxsig_validate(cf->ctx, 0, resp->Signature, 1, &refs);
      if (ret == ZXSIG_BAD_CERT) {
	INFO("Canon sha1 of Response verified OK %d", ret);
	if (verbose)
	  printf("\nCanon sha1 of Response verified OK %d\n", ret);
      } else {
	ERR("Response Signature hash validation error. Bad canonicalization? ret=%d",ret);
	if (sig_flag < 3)
	  return 10;
      }
    }
  } else {
    if (!zxid_chk_sig(cf, &cgi, &ses, &resp->gg, resp->Signature, resp->Issuer, 0, "Response"))
      return 4;
  }
  
  a7n = zxid_dec_a7n(cf, resp->Assertion, resp->EncryptedAssertion);
  if (!a7n) {
    ERR("No Assertion found and not anon_ok in SAML Response %d", 0);
    return 5;
  }
  zx_see_elem_ns(cf->ctx, &pop_seen, &resp->gg);
got_a7n:
  if (sig_flag == 2) {
    if (a7n->Signature && a7n->Signature->SignedInfo && a7n->Signature->SignedInfo->Reference) {
      zx_reset_ns_ctx(cf->ctx);
      ZERO(&refs, sizeof(refs));
      refs.sref = a7n->Signature->SignedInfo->Reference;
      refs.blob = &a7n->gg;
      refs.pop_seen = pop_seen;
      zx_see_elem_ns(cf->ctx, &refs.pop_seen, &a7n->gg);
      ret = zxsig_validate(cf->ctx, 0, a7n->Signature, 1, &refs);
      if (ret == ZXSIG_BAD_CERT) {
	INFO("Canon sha1 of Assertion verified OK %d", ret);
	if (ret && verbose)
	  printf("\nCanon sha1 of Assertion verified OK %d\n", ret);
	return 0;
      }
      ERR("Canon sha1 of Assertion failed to verify ret=%d", ret);
      return 11;
    } else {
      ERR("Assertion does not contain a signature %p", a7n->Signature);
      return 7;
    }
  } else {
    ret = zxid_sp_sso_finalize(cf, &cgi, &ses, a7n, pop_seen);
    INFO("zxid_sp_sso_finalize() returned %d", ret);
  }
  if (ret && verbose)
    printf("\nSIG Verified OK, zxid_sp_sso_finalize() returned %d\n", ret);
  return ret?0:6;
}

/* Called by:  zxdecode_main x4 */
static int decode(char* msg, char* q)
{
  int len;
  char* p;
  char* m2;
  char* p2;
  
  *q = 0;
  D("Original Msg(%s) x=%x", msg, *msg);
  
  if (strchr(msg, '%')) {
    p = p2 = msg;
    URL_DECODE(p, p2, q);
    q = p;
    *q = 0;
    D("URL Decoded Msg(%s) x=%x", msg, *msg);
  } else
    p = q;
  
  switch (b64_flag) {
  case 0:
    D("decode_base64 skipped at user request %d",0);
    break;
  case 1:
    D("decode_base64 forced at user request %d",0);
b64_dec:
    /* msglen = q - msg; */
    p = unbase64_raw(msg, q, msg, zx_std_index_64);  /* inplace */
    *p = 0;
    D("Unbase64 Msg(%s) x=%x len=%d (n.b. message data may be binary at this point)", msg, *msg, ((int)(p-msg)));
    break;
  case 2:
    if (*msg == '<') {
      D("decode_base64 auto detect: no decode due to initial < %p %p", msg, p);
    } else {
      D("decode_base64 auto detect: decode due to initial 0x%x", *msg);
      goto b64_dec;
    }
    break;
  }
  
  switch (inflate_flag) {
  case 0:
    len = p-msg;
    p = msg;
    D("No decompression by user choice len=%d", len);
    break;
  case 1:
    D("Decompressing... (force) %d",0);
decompress:
    p = zx_zlib_raw_inflate(0, p-msg, msg, &len);  /* Redir uses compressed payload. */
    break;
  case 2:
    /* Skip whitespace in the beginning and end of the payload to help correct POST detection. */
    for (m2 = msg; m2 < p; ++m2)
      if (!ONE_OF_4(*m2, ' ', '\t', '\015', '\012'))
	break;
    for (p2 = p-1; m2 < p2; --p2)
      if (!ONE_OF_4(*p2, ' ', '\t', '\015', '\012'))
	break;
    D("Msg_minus_whitespace(%.*s) start=%x end=%x", ((int)(p2-m2+1)), m2, *m2, *p2);
    
    if (*m2 == '<' && *p2 == '>') {  /* POST profiles do not compress the payload */
      len = p2 - m2 + 1;
      p = m2;
    } else {
      D("Decompressing... (auto) %d",0);
      goto decompress;
    }
    break;
  }
  fwrite(p, 1, len, stdout);
  
  if (sig_flag)
    return sig_validate(len, p);
  return 0;
}

#ifndef zxdecode_main
#define zxdecode_main main
#endif

/* Called by: */
int zxdecode_main(int argc, char** argv, char** env)
{
  int got;
  char* pp;
  char* p;
  char* q;
  char* lim;

  strcpy(errmac_instance, "\tzxdec");
  opt(&argc, &argv, &env);

  read_all_fd(fdstdin, buf, sizeof(buf)-1, &got);
  buf[got] = 0;
  lim = buf+got;

  if (sha1_flag) {
    p = sha1_safe_base64(buf, got, buf);
    *p = 0;
    printf("%s\n", buf);
    return 0;
  }

  if (resp_flag)
    return sig_validate(got, buf);

  if (valid_opt)
    return ws_validations();

  /* Try to detect relevant input, iterating if -i N was specified.
   * The detection is supposed to pick SAMLRequest or SAMLResponse from
   * middle of HTML form, or from log output. Whatever is convenient. */

  for (pp = buf; pp && pp < lim; pp = p+1) {
    p = strstr(pp, "SAMLRequest=");
    if (p) {
      if (--ix)	continue;
      q = strchr(p, '&');
      return decode(p + sizeof("SAMLRequest=")-1, q?q:lim);
    }
    p = strstr(pp, "SAMLResponse=");
    if (p) {
      if (--ix)	continue;
      q = strchr(p, '&');
      return decode(p + sizeof("SAMLResponse=")-1, q?q:lim);
    }
    if (*pp == '<') {  /* HTML for POST */
      p = strstr(pp, "SAMLRequest");
      if (p) {
	p += sizeof("SAMLRequest")-1;
      } else {
	p = strstr(pp, "SAMLResponse");
	if (p)
	  p += sizeof("SAMLResponse")-1;
      }
      if (p) {
	p = strstr(p, "value=");
	if (p) {
	  if (--ix)	continue;
	  p += sizeof("value=")-1;
	  if (*p == '"') {
	    ++p;
	    q = strchr(p, '"');
	  } else {
	    q = p+strcspn(p, "\" >");
	  }
	  return decode(p, q?q:lim);
	}
      }
    }
    if (--ix) { p = pp; continue; }
    return decode(pp, lim);  /* Decode the object identified above. */
  }
  ERR("No SAMLRequest or SAMLResponse found to decode %p %p %p", buf, pp, lim);
  return 1;
}

/* EOF  --  zxdecode.c */