Skip to content

Instantly share code, notes, and snippets.

@ivarprudnikov
Created February 29, 2024 12:14
Show Gist options
  • Save ivarprudnikov/a368f5a2c4c9ac4eaaa1e45409ed1425 to your computer and use it in GitHub Desktop.
Save ivarprudnikov/a368f5a2c4c9ac4eaaa1e45409ed1425 to your computer and use it in GitHub Desktop.
Create COSE_Sign1 signature envelope in Go and sign with a key in Azure KeyVault
module something.com/whatever
go 1.19
require github.com/veraison/go-cose v1.1.1-0.20240126165338-2300d5c96dbd
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.20.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
package main
import (
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
"github.com/veraison/go-cose"
)
// https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/
const ISSUER_HEADER_KEY = int64(391)
const ISSUER_HEADER_FEED = int64(392)
const ISSUER_HEADER_REG_INFO = int64(393)
const HASH_ALG = cose.AlgorithmPS256
var kvUrl string
var keyName string
var keyVer string
var payloadPath string
var outputFilePath string
func init() {
flag.StringVar(&kvUrl, "kv-url", "https://myvault.vault.azure.net", "Key Vault URL")
flag.StringVar(&keyName, "key-name", "generatedcert", "Key name")
flag.StringVar(&keyVer, "key-ver", "abc123", "Key version")
flag.StringVar(&payloadPath, "payload", "foobar.txt", "Payload path")
flag.StringVar(&outputFilePath, "output", "foobar.sig.cose", "Output file path")
}
func main() {
// parse args
flag.Parse()
pwd, _ := os.Getwd()
fmt.Println("pwd:", pwd)
fmt.Println("Key Vault URL:", kvUrl)
fmt.Println("Key Name:", keyName)
fmt.Println("Key Version:", keyVer)
fmt.Println("Payload Path:", payloadPath)
fmt.Println("Output Path:", outputFilePath)
// prepare keyvault digest signer
signer := &akvSigner{
keyVaultUrl: kvUrl,
keyName: keyName,
keyVer: keyVer,
}
// read payload
payload, err := os.ReadFile(payloadPath)
if err != nil {
fmt.Printf("failed to read payload:\n%v\n", err)
os.Exit(1)
}
// sign
sig, err := CreateSignature(payload, "text/plain", signer)
if err != nil {
fmt.Printf("failed to create sig:\n%v\n", err)
os.Exit(1)
}
// save
err = os.WriteFile(outputFilePath, sig, 0644)
if err != nil {
fmt.Printf("failed to write sig to file:\n%v\n", err)
os.Exit(1)
}
}
// implement crypto.Signer
type akvSigner struct {
keyVaultUrl string
keyName string
keyVer string
}
func (s *akvSigner) keyClient() (*azkeys.Client, error) {
// create credential
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}
// create azkeys client
client, err := azkeys.NewClient(s.keyVaultUrl, cred, nil)
if err != nil {
return nil, err
}
return client, nil
}
func (s *akvSigner) certClient() (*azcertificates.Client, error) {
// create credential
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}
// create azkeys client
client, err := azcertificates.NewClient(s.keyVaultUrl, cred, nil)
if err != nil {
return nil, err
}
return client, nil
}
// certs returns the certificate chain for the signer
// it will attempt to download the CA from a url in the x509 extension
func (s *akvSigner) certs() [][]byte {
c, err := s.certClient()
if err != nil {
fmt.Printf("failed to init cert client:\n%v\n", err)
os.Exit(1)
}
// get the cert representing the signer
resp, err := c.GetCertificate(context.Background(), keyName, keyVer, nil)
if err != nil {
fmt.Printf("failed to get cert:\n%v\n", err)
os.Exit(1)
}
certs := [][]byte{resp.Certificate.CER}
// check if the cert contains IssuingCertificateURL
cert, err := x509.ParseCertificate(resp.Certificate.CER)
if err != nil {
fmt.Printf("failed to parse the cert:\n%v\n", err)
os.Exit(1)
}
fmt.Println(cert.IssuingCertificateURL)
if len(cert.IssuingCertificateURL) > 0 {
// get the issuing cert
issResp, err := http.DefaultClient.Get(cert.IssuingCertificateURL[0])
if err != nil {
fmt.Printf("failed to download issuing cert:\n%v\n", err)
os.Exit(1)
}
if issResp.StatusCode >= 200 && issResp.StatusCode < 300 {
pemContent, err := io.ReadAll(issResp.Body)
if err != nil {
fmt.Printf("failed to read downloaded body:\n%v\n", err)
os.Exit(1)
}
// parse pem data
certData := pemContent
for {
var block *pem.Block
block, certData = pem.Decode(certData)
if block == nil {
break
}
certs = append(certs, block.Bytes)
}
} else {
fmt.Printf("failed to download issuing cert:\n%v\n", issResp.Status)
os.Exit(1)
}
} else {
fmt.Println("no issuing cert, adding itself to the chain to act as a root CA.")
certs = append(certs, resp.Certificate.CER)
}
return certs
}
func (s *akvSigner) Public() crypto.PublicKey {
certs := s.certs()
if len(certs) == 0 {
fmt.Printf("no certs found\n")
os.Exit(1)
}
// parse the signer cert
cert, err := x509.ParseCertificate(certs[0])
if err != nil {
fmt.Printf("failed to parse the first cert in ca bundle:\n%v\n", err)
os.Exit(1)
}
return cert.PublicKey
}
func (s *akvSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
client, err := s.keyClient()
if err != nil {
return nil, err
}
a := azkeys.SignatureAlgorithm(HASH_ALG.String())
// sign content
sigResp, err := client.Sign(context.Background(), s.keyName, s.keyVer, azkeys.SignParameters{
Algorithm: &a,
Value: digest,
}, nil)
if err != nil {
return nil, err
}
return sigResp.Result, nil
}
func CreateSignature(content []byte, contentType string, keySigner *akvSigner) ([]byte, error) {
// create AKV digest signer
signer, err := cose.NewSigner(HASH_ALG, keySigner)
if err != nil {
return nil, fmt.Errorf("failed to create signer: %w", err)
}
// create message header
headers := cose.Headers{
Protected: cose.ProtectedHeader{
cose.HeaderLabelAlgorithm: HASH_ALG,
cose.HeaderLabelContentType: contentType,
ISSUER_HEADER_FEED: "demo",
ISSUER_HEADER_REG_INFO: map[interface{}]interface{}{
"register_by": uint64(time.Now().Add(24 * time.Hour).Unix()),
"sequence_no": uint64(1),
"issuance_ts": uint64(time.Now().Unix()),
},
cose.HeaderLabelX5Chain: keySigner.certs(),
},
}
// sign and marshal message
return cose.Sign1(rand.Reader, signer, headers, content, nil)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment