Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions cmd/sign1util/ccf_keyfetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package main

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"

"github.com/Microsoft/cosesign1go/pkg/cosesign1"
)

// jwk is a minimal JSON Web Key representation used to parse CCF transparency
// service /jwks responses.
type jwk struct {
Kty string `json:"kty"`
Crv string `json:"crv"`
Kid string `json:"kid"`
X string `json:"x"`
Y string `json:"y"`
}

type jwkSet struct {
Keys []jwk `json:"keys"`
}

func jwkToPublicKey(k jwk) (crypto.PublicKey, error) {
if k.Kty != "EC" {
return nil, fmt.Errorf("unsupported kty %q", k.Kty)
}
var curve elliptic.Curve
switch k.Crv {
case "P-256":
curve = elliptic.P256()
case "P-384":
curve = elliptic.P384()
case "P-521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("unsupported curve %q", k.Crv)
}
xBytes, err := base64.RawURLEncoding.DecodeString(k.X)
if err != nil {
return nil, fmt.Errorf("decoding x: %w", err)
}
yBytes, err := base64.RawURLEncoding.DecodeString(k.Y)
if err != nil {
return nil, fmt.Errorf("decoding y: %w", err)
}
return &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(xBytes),
Y: new(big.Int).SetBytes(yBytes),
}, nil
}

// CertVerifier is invoked with the leaf certificate presented by a CCF node
// during TLS handshake. Returning nil accepts the certificate; returning an
// error aborts the connection.
type CertVerifier func(issuer string, cert *x509.Certificate) error

// fetchIssuerJWKS GETs https://<issuer>/jwks and returns the keys keyed by
// their `kid`. If verifyCert is not nil, the leaf certificate presented by the
// server is passed to verifyCert.
func fetchIssuerJWKS(issuer string, verifyCert CertVerifier) (map[string]crypto.PublicKey, error) {
url := "https://" + issuer + "/jwks"
tlsConfig := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // CCF uses self-signed certs that are supposed to be validated via attestation, and so will never pass the normal verification.

Check failure

Code scanning / CodeQL

Disabled TLS certificate check High

InsecureSkipVerify should not be used in production code.
Comment thread
micromaomao marked this conversation as resolved.
Dismissed
if verifyCert != nil {
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("server presented no certificate")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("parsing server certificate: %w", err)
}
return verifyCert(issuer, cert)
}
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
Comment thread
micromaomao marked this conversation as resolved.
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", url, err)
}
var set jwkSet
if err := json.Unmarshal(body, &set); err != nil {
return nil, fmt.Errorf("parsing %s: %w", url, err)
}
Comment on lines +101 to +108
out := make(map[string]crypto.PublicKey, len(set.Keys))
for i, k := range set.Keys {
pub, err := jwkToPublicKey(k)
if err != nil {
return nil, fmt.Errorf("key %d (kid=%s): %w", i, k.Kid, err)
}
if existingKey, exists := out[k.Kid]; exists {
// Equal is implemented for all crypto.PublicKey types in std
eq, ok := existingKey.(interface{ Equal(crypto.PublicKey) bool })
if !ok || !eq.Equal(pub) {
return nil, fmt.Errorf("conflicting kid %s seen in JWKS from %s", k.Kid, url)
}
continue
}
out[k.Kid] = pub
}
return out, nil
}

// fetchCCFReceiptKeys returns a kid->PublicKey map by fetching the JWKS for
// each unique receipt issuer. If not nil, verifyCert is invoked with the leaf
// certificate presented by each issuer.
func fetchCCFReceiptKeys(receipts []cosesign1.ParsedCOSEReceipt, verifyCert CertVerifier) (map[string]crypto.PublicKey, error) {
seen := map[string]bool{}
keys := map[string]crypto.PublicKey{}
for _, r := range receipts {
if r.Issuer == "" {
return nil, fmt.Errorf("receipt has no issuer; cannot fetch JWKS")
}
if seen[r.Issuer] {
continue
}
seen[r.Issuer] = true
issuerKeys, err := fetchIssuerJWKS(r.Issuer, verifyCert)
if err != nil {
return nil, err
}
for kid, k := range issuerKeys {
if _, exists := keys[kid]; exists {
return nil, fmt.Errorf("Issuer %s JWKS contains kid %s which is already present from another issuer", r.Issuer, kid)
}
keys[kid] = k
}
}
return keys, nil
}
159 changes: 158 additions & 1 deletion cmd/sign1util/main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,63 @@
package main

import (
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"fmt"
"os"
"strings"

"github.com/Microsoft/cosesign1go/pkg/cosesign1"
didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)

// formatValue formats a CBOR-decoded value in a human-readable way that
// preserves integer keys (unlike JSON).
func formatValue(v interface{}) string {
switch v := v.(type) {
case map[interface{}]interface{}:
parts := make([]string, 0, len(v))
for key, val := range v {
parts = append(parts, fmt.Sprintf("%s: %s", formatValue(key), formatValue(val)))
}
return "{" + strings.Join(parts, ", ") + "}"
case []interface{}:
parts := make([]string, 0, len(v))
for _, val := range v {
parts = append(parts, formatValue(val))
}
return "[" + strings.Join(parts, ", ") + "]"
case []byte:
return fmt.Sprintf("0x%x", v)
case string:
return fmt.Sprintf("%q", v)
case nil:
return "null"
default:
return fmt.Sprintf("%v", v)
}
}

func printKeyValue(indent string, k, v interface{}) {
fmt.Fprintf(os.Stdout, "%s%v: %s\n", indent, k, formatValue(v))
}

// CCF nodes serve TLS using a self-signed certificate whose authenticity is
// backed by attestation rather than a public CA. Since we have no way to
// validate the CCF's attestation evidence here, we simply prints summary
Comment thread
micromaomao marked this conversation as resolved.
Outdated
// details of a CCF node's TLS certificate and unconditionally accepts it, which
// is acceptable for this tool.
func acceptAndPrintCert(issuer string, cert *x509.Certificate) error {
fp := sha256.Sum256(cert.Raw)
fmt.Fprintf(os.Stdout, "%s: accepting TLS certificate subject=%q issuer=%q notAfter=%s sha256=%s\n",
issuer, cert.Subject, cert.Issuer, cert.NotAfter.Format("2006-01-02"), hex.EncodeToString(fp[:]))
return nil
}

func checkCoseSign1(inputFilename string, chainFilename string, didString string, verbose bool) (*cosesign1.UnpackedCoseSign1, error) {
coseBlob, err := os.ReadFile(inputFilename)
if err != nil {
Expand All @@ -33,13 +81,57 @@ func checkCoseSign1(inputFilename string, chainFilename string, didString string
}

fmt.Fprint(os.Stdout, "checkCoseSign1 passed\n")

// If the envelope carries COSE Receipts, validate each against the CCF
// ledger profile.
var receiptKeys map[string]crypto.PublicKey
if len(unpacked.Receipts) > 0 {
receiptKeys, err = fetchCCFReceiptKeys(unpacked.Receipts, acceptAndPrintCert)
if err != nil {
fmt.Fprintf(os.Stdout, "fetching CCF receipt keys failed - %s\n", err)
return nil, fmt.Errorf("fetching CCF receipt keys: %w", err)
}
for i, r := range unpacked.Receipts {
if err := r.Validate(receiptKeys); err != nil {
fmt.Fprintf(os.Stdout, "CCF receipt %d from %s validation failed - %s\n", i, r.Issuer, err)
return nil, fmt.Errorf("CCF receipt %d from %s validation failed: %w", i, r.Issuer, err)
}
fmt.Fprintf(os.Stdout, "CCF receipt %d from %s validation passed\n", i, r.Issuer)
}
}
if verbose {
fmt.Fprintf(os.Stdout, "iss: %s\n", unpacked.Issuer)
fmt.Fprintf(os.Stdout, "feed: %s\n", unpacked.Feed)
fmt.Fprintf(os.Stdout, "cty: %s\n", unpacked.ContentType)
fmt.Fprintf(os.Stdout, "pubkey: %s\n", unpacked.Pubkey)
fmt.Fprintf(os.Stdout, "pubcert: %s\n", unpacked.Pubcert)
fmt.Fprintf(os.Stdout, "payload:\n%s\n", string(unpacked.Payload[:]))
fmt.Fprintf(os.Stdout, "all protected headers:\n")
isHashEnvelope := false
for k, v := range unpacked.Protected {
if k, ok := k.(int64); ok && (k == cosesign1.COSE_Header_x5chain || k == cosesign1.COSE_Header_x5t) {
fmt.Fprintf(os.Stdout, " %d: ...\n", k)
continue
}
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_PreimageContentType {
isHashEnvelope = true
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, "all unprotected headers:\n")
for k, v := range unpacked.Unprotected {
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_Receipts {
fmt.Fprintf(os.Stdout, " %d: ...\n", k)
continue
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, "payload:\n")
if isHashEnvelope {
fmt.Fprintf(os.Stdout, "%x", unpacked.Payload[:])
} else {
fmt.Fprintf(os.Stdout, "%s", string(unpacked.Payload))
}
fmt.Fprintf(os.Stdout, "\n")
}
if len(didString) > 0 {
if len(chainPEMString) == 0 {
Expand All @@ -53,6 +145,55 @@ func checkCoseSign1(inputFilename string, chainFilename string, didString string
fmt.Fprintf(os.Stdout, "DID resolvers failed: err: %s\n", err.Error())
}
}

for i, receipt := range unpacked.Receipts {
if !verbose {
continue
}
msg := receipt.Message
fmt.Fprintf(os.Stdout, "receipt %d:\n", i)
fmt.Fprintf(os.Stdout, " protected headers:\n")
for k, v := range msg.Headers.Protected {
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_kid {
switch v := v.(type) {
case []byte:
fmt.Fprintf(os.Stdout, " %d: %q\n", k, v)
case string:
fmt.Fprintf(os.Stdout, " %d: string(%q) (invalid type for kid)\n", k, v)
default:
fmt.Fprintf(os.Stdout, " %d: ... (invalid type for kid)\n", k)
}
continue
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, " unprotected headers:\n")
for k, v := range msg.Headers.Unprotected {
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_vdp {
m, ok := v.(map[interface{}]interface{})
if !ok {
fmt.Fprintf(os.Stdout, " %d: ... (invalid type for vdp)\n", k)
continue
}
fmt.Fprintf(os.Stdout, " %d:\n", k)
for k, v := range m {
if k, ok := k.(int64); ok && k == cosesign1.COSE_ProofInclusion {
fmt.Fprintf(os.Stdout, " %d (inclusion): ...\n", k)
continue
}
if k, ok := k.(int64); ok && k == cosesign1.COSE_ProofConsistency {
fmt.Fprintf(os.Stdout, " %d (consistency): ...\n", k)
continue
}
printKeyValue(" ", k, v)
}
continue
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, " payload: %q\n", msg.Payload)
}

return unpacked, err
}

Expand Down Expand Up @@ -367,6 +508,22 @@ var chainCmd = cli.Command{
func main() {
app := cli.NewApp()
app.Name = "sign1util"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "log-level",
Usage: "logrus log level [trace|debug|info|warn|error]",
EnvVar: "LOG_LEVEL",
Value: "info",
},
}
app.Before = func(ctx *cli.Context) error {
lvl, err := logrus.ParseLevel(ctx.GlobalString("log-level"))
if err != nil {
return fmt.Errorf("invalid --log-level: %w", err)
}
logrus.SetLevel(lvl)
return nil
}
app.Commands = []cli.Command{
createCmd,
checkCmd,
Expand Down
Loading
Loading