fleet/third_party/httpsig-go/keyutil/keyutil.go
Victor Lyuboslavsky c25fed2492
Added a vendored version of httpsig-go. (#30820)
For #30473

This change adds a vendored `httpsig-go` library to our repo. We cannot
use the upstream library because it has not merged the change we need:
https://github.com/remitly-oss/httpsig-go/pull/25

Thus, we need our own copy at this point.

The instructions for keeping this library up to date (if needed) are in
`UPDATE_INSTRUCTIONS`.

None of the coderabbitai review comments are relevant to the
code/features we are going to use for HTTP message signatures.

We will use this library in subsequent PRs for the TPM-backed HTTP
message signature feature.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a Go library for HTTP message signing and verification,
supporting multiple cryptographic algorithms (RSA, ECDSA, Ed25519,
HMAC).
* Added utilities for key management, including JWK and PEM key
handling.
* Provided HTTP client and server helpers for automatic request signing
and signature verification.
* Implemented structured error handling and metadata extraction for
signatures.

* **Documentation**
  * Added comprehensive README, usage examples, and update instructions.
* Included license and configuration files for third-party and testing
tools.

* **Tests**
* Added extensive unit, integration, and fuzz tests covering signing,
verification, and key handling.
* Included official RFC test vectors and various test data files for
robust validation.

* **Chores**
* Integrated continuous integration workflows and ignore files for code
quality and security analysis.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 20:26:50 +02:00

143 lines
4 KiB
Go

/*
Package keyutil provides some basic PEM and JWK key handling without dependencies. It is not meant as a replacement for a full key handling library.
*/
package keyutil
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"os"
)
var (
oidPublicKeyRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10}
)
// MustReadPublicKeyFile reads a PEM encoded public key file or panics
func MustReadPublicKeyFile(pubkeyFile string) crypto.PublicKey {
pk, err := ReadPublicKeyFile(pubkeyFile)
if err != nil {
panic(err)
}
return pk
}
// ReadPublicKeyFile reads a PEM encdoded public key file and parses into crypto.PublicKey
func ReadPublicKeyFile(pubkeyFile string) (crypto.PublicKey, error) {
keyBytes, err := os.ReadFile(pubkeyFile)
if err != nil {
return nil, fmt.Errorf("Failed to read public key file '%s': %w", pubkeyFile, err)
}
return ReadPublicKey(keyBytes)
}
// ReadPublicKey decodes a PEM encoded public key and parses into crypto.PublicKey
func ReadPublicKey(encodedPubkey []byte) (crypto.PublicKey, error) {
block, _ := pem.Decode(encodedPubkey)
if block == nil {
return nil, fmt.Errorf("Failed to PEM decode public key")
}
var key crypto.PublicKey
var err error
switch block.Type {
case "PUBLIC KEY":
key, err = x509.ParsePKIXPublicKey(block.Bytes)
case "RSA PUBLIC KEY":
key, err = x509.ParsePKCS1PublicKey(block.Bytes)
default:
return nil, fmt.Errorf("Unsupported pubkey format '%s'", block.Type)
}
if err != nil {
return nil, fmt.Errorf("Failed to parse public key with format '%s': %w", block.Type, err)
}
return key, nil
}
// MustReadPrivateKeyFile decodes a PEM encoded private key file and parses into a crypto.PrivateKey or panics.
func MustReadPrivateKeyFile(pkFile string) crypto.PrivateKey {
pk, err := ReadPrivateKeyFile(pkFile)
if err != nil {
panic(err)
}
return pk
}
func MustReadPrivateKey(encodedPrivateKey []byte) crypto.PrivateKey {
pkey, err := ReadPrivateKey(encodedPrivateKey)
if err != nil {
panic(err)
}
return pkey
}
// ReadPrivateKeyFile opens the given file and calls ReadPrivateKey to return a crypto.PrivateKey
func ReadPrivateKeyFile(pkFile string) (crypto.PrivateKey, error) {
keyBytes, err := os.ReadFile(pkFile)
if err != nil {
return nil, fmt.Errorf("Failed to read private key file '%s': %w", pkFile, err)
}
return ReadPrivateKey(keyBytes)
}
// ReadPrivateKey decoded a PEM encoded private key and parses into a crypto.PrivateKey.
func ReadPrivateKey(encodedPrivateKey []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(encodedPrivateKey)
if block == nil {
return nil, fmt.Errorf("Failed to PEM decode private key")
}
var key crypto.PrivateKey
var err error
switch block.Type {
case "PRIVATE KEY":
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Try to handle RSAPSS
psskey, psserr := parseRSAPSS(block)
if psserr == nil {
// success
key = psskey
err = psserr
}
}
case "EC PRIVATE KEY":
key, err = x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("Unsupported private key format '%s'", block.Type)
}
if err != nil {
return nil, fmt.Errorf("Failed to parse private key with format '%s': %w", block.Type, err)
}
return key, nil
}
func parseRSAPSS(block *pem.Block) (crypto.PrivateKey, error) {
// The rsa-pss key is PKCS8 encoded but the golang 1.19 parser doesn't recognize the algorithm and gives 'PKCS#8 wrapping contained private key with unknown algorithm: 1.2.840.113549.1.1.10
// Instead do the asn1 unmarshaling and check here.
pkcs8 := struct {
Version int
Algo pkix.AlgorithmIdentifier
PrivateKey []byte
}{}
_, err := asn1.Unmarshal(block.Bytes, &pkcs8)
if err != nil {
return nil, fmt.Errorf("Failed to ans1 unmarshal private key: %w", err)
}
if !pkcs8.Algo.Algorithm.Equal(oidPublicKeyRSAPSS) {
return nil, fmt.Errorf("PKCS#8 wrapping contained private key with unknown algorithm: %s", pkcs8.Algo.Algorithm)
}
return x509.ParsePKCS1PrivateKey(pkcs8.PrivateKey)
}