mirror of
https://github.com/pion/webrtc.git
synced 2025-09-26 19:21:12 +08:00

Update CertificateFromPEM to be a loop over the PEM blocks. This allows decoding the private key before decoding the certificate. Tries to parse the certificate block directly but if that errors, then it also tries to base64 decode the certificate block. Fixes #3042
269 lines
7.6 KiB
Go
269 lines
7.6 KiB
Go
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !js
|
|
// +build !js
|
|
|
|
package webrtc
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pion/dtls/v3/pkg/crypto/fingerprint"
|
|
"github.com/pion/webrtc/v4/pkg/rtcerr"
|
|
)
|
|
|
|
// Certificate represents a x509Cert used to authenticate WebRTC communications.
|
|
type Certificate struct {
|
|
privateKey crypto.PrivateKey
|
|
x509Cert *x509.Certificate
|
|
statsID string
|
|
}
|
|
|
|
// NewCertificate generates a new x509 compliant Certificate to be used
|
|
// by DTLS for encrypting data sent over the wire. This method differs from
|
|
// GenerateCertificate by allowing to specify a template x509.Certificate to
|
|
// be used in order to define certificate parameters.
|
|
func NewCertificate(key crypto.PrivateKey, tpl x509.Certificate) (*Certificate, error) {
|
|
var err error
|
|
var certDER []byte
|
|
switch sk := key.(type) {
|
|
case *rsa.PrivateKey:
|
|
pk := sk.Public()
|
|
tpl.SignatureAlgorithm = x509.SHA256WithRSA
|
|
certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk)
|
|
if err != nil {
|
|
return nil, &rtcerr.UnknownError{Err: err}
|
|
}
|
|
case *ecdsa.PrivateKey:
|
|
pk := sk.Public()
|
|
tpl.SignatureAlgorithm = x509.ECDSAWithSHA256
|
|
certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk)
|
|
if err != nil {
|
|
return nil, &rtcerr.UnknownError{Err: err}
|
|
}
|
|
default:
|
|
return nil, &rtcerr.NotSupportedError{Err: ErrPrivateKeyType}
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return nil, &rtcerr.UnknownError{Err: err}
|
|
}
|
|
|
|
return &Certificate{
|
|
privateKey: key,
|
|
x509Cert: cert,
|
|
statsID: fmt.Sprintf("certificate-%d", time.Now().UnixNano()),
|
|
}, nil
|
|
}
|
|
|
|
// Equals determines if two certificates are identical by comparing both the
|
|
// secretKeys and x509Certificates.
|
|
func (c Certificate) Equals(cert Certificate) bool {
|
|
switch cSK := c.privateKey.(type) {
|
|
case *rsa.PrivateKey:
|
|
if oSK, ok := cert.privateKey.(*rsa.PrivateKey); ok {
|
|
if cSK.N.Cmp(oSK.N) != 0 {
|
|
return false
|
|
}
|
|
|
|
return c.x509Cert.Equal(cert.x509Cert)
|
|
}
|
|
|
|
return false
|
|
case *ecdsa.PrivateKey:
|
|
if oSK, ok := cert.privateKey.(*ecdsa.PrivateKey); ok {
|
|
if cSK.X.Cmp(oSK.X) != 0 || cSK.Y.Cmp(oSK.Y) != 0 {
|
|
return false
|
|
}
|
|
|
|
return c.x509Cert.Equal(cert.x509Cert)
|
|
}
|
|
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Expires returns the timestamp after which this certificate is no longer valid.
|
|
func (c Certificate) Expires() time.Time {
|
|
if c.x509Cert == nil {
|
|
return time.Time{}
|
|
}
|
|
|
|
return c.x509Cert.NotAfter
|
|
}
|
|
|
|
// GetFingerprints returns the list of certificate fingerprints, one of which
|
|
// is computed with the digest algorithm used in the certificate signature.
|
|
func (c Certificate) GetFingerprints() ([]DTLSFingerprint, error) {
|
|
fingerprintAlgorithms := []crypto.Hash{crypto.SHA256}
|
|
res := make([]DTLSFingerprint, len(fingerprintAlgorithms))
|
|
|
|
i := 0
|
|
for _, algo := range fingerprintAlgorithms {
|
|
name, err := fingerprint.StringFromHash(algo)
|
|
if err != nil {
|
|
// nolint
|
|
return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err)
|
|
}
|
|
value, err := fingerprint.Fingerprint(c.x509Cert, algo)
|
|
if err != nil {
|
|
// nolint
|
|
return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err)
|
|
}
|
|
res[i] = DTLSFingerprint{
|
|
Algorithm: name,
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
return res[:i+1], nil
|
|
}
|
|
|
|
// GenerateCertificate causes the creation of an X.509 certificate and
|
|
// corresponding private key.
|
|
func GenerateCertificate(secretKey crypto.PrivateKey) (*Certificate, error) {
|
|
// Max random value, a 130-bits integer, i.e 2^130 - 1
|
|
maxBigInt := new(big.Int)
|
|
/* #nosec */
|
|
maxBigInt.Exp(big.NewInt(2), big.NewInt(130), nil).Sub(maxBigInt, big.NewInt(1))
|
|
/* #nosec */
|
|
serialNumber, err := rand.Int(rand.Reader, maxBigInt)
|
|
if err != nil {
|
|
return nil, &rtcerr.UnknownError{Err: err}
|
|
}
|
|
|
|
return NewCertificate(secretKey, x509.Certificate{
|
|
Issuer: pkix.Name{CommonName: generatedCertificateOrigin},
|
|
NotBefore: time.Now().AddDate(0, 0, -1),
|
|
NotAfter: time.Now().AddDate(0, 1, -1),
|
|
SerialNumber: serialNumber,
|
|
Version: 2,
|
|
Subject: pkix.Name{CommonName: generatedCertificateOrigin},
|
|
})
|
|
}
|
|
|
|
// CertificateFromX509 creates a new WebRTC Certificate from a given PrivateKey and Certificate
|
|
//
|
|
// This can be used if you want to share a certificate across multiple PeerConnections.
|
|
func CertificateFromX509(privateKey crypto.PrivateKey, certificate *x509.Certificate) Certificate {
|
|
return Certificate{privateKey, certificate, fmt.Sprintf("certificate-%d", time.Now().UnixNano())}
|
|
}
|
|
|
|
func (c Certificate) collectStats(report *statsReportCollector) error {
|
|
report.Collecting()
|
|
|
|
fingerPrintAlgo, err := c.GetFingerprints()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
base64Certificate := base64.RawURLEncoding.EncodeToString(c.x509Cert.Raw)
|
|
|
|
stats := CertificateStats{
|
|
Timestamp: statsTimestampFrom(time.Now()),
|
|
Type: StatsTypeCertificate,
|
|
ID: c.statsID,
|
|
Fingerprint: fingerPrintAlgo[0].Value,
|
|
FingerprintAlgorithm: fingerPrintAlgo[0].Algorithm,
|
|
Base64Certificate: base64Certificate,
|
|
IssuerCertificateID: c.x509Cert.Issuer.String(),
|
|
}
|
|
|
|
report.Collect(stats.ID, stats)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CertificateFromPEM creates a fresh certificate based on a string containing
|
|
// pem blocks fort the private key and x509 certificate.
|
|
func CertificateFromPEM(pems string) (*Certificate, error) { //nolint: cyclop
|
|
var cert *x509.Certificate
|
|
var privateKey crypto.PrivateKey
|
|
|
|
var block *pem.Block
|
|
more := []byte(pems)
|
|
for {
|
|
var err error
|
|
block, more = pem.Decode(more)
|
|
if block == nil {
|
|
break
|
|
}
|
|
|
|
// decode & parse the certificate
|
|
switch block.Type {
|
|
case "CERTIFICATE":
|
|
if cert != nil {
|
|
return nil, errCertificatePEMMultipleCert
|
|
}
|
|
cert, err = x509.ParseCertificate(block.Bytes)
|
|
// If parsing failed using block.Bytes, then parse the bytes as base64 and try again
|
|
if err != nil {
|
|
var n int
|
|
certBytes := make([]byte, base64.StdEncoding.DecodedLen(len(block.Bytes)))
|
|
n, err = base64.StdEncoding.Decode(certBytes, block.Bytes)
|
|
if err == nil {
|
|
cert, err = x509.ParseCertificate(certBytes[:n])
|
|
}
|
|
}
|
|
case "PRIVATE KEY":
|
|
if privateKey != nil {
|
|
return nil, errCertificatePEMMultiplePriv
|
|
}
|
|
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
}
|
|
|
|
// Report errors from parsing either the private key or the certificate
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode %s: %w", block.Type, err)
|
|
}
|
|
}
|
|
|
|
if cert == nil || privateKey == nil {
|
|
return nil, errCertificatePEMMissing
|
|
}
|
|
|
|
ret := CertificateFromX509(privateKey, cert)
|
|
|
|
return &ret, nil
|
|
}
|
|
|
|
// PEM returns the certificate encoded as two pem block: once for the X509
|
|
// certificate and the other for the private key.
|
|
func (c Certificate) PEM() (string, error) {
|
|
// First write the X509 certificate
|
|
var builder strings.Builder
|
|
xcertBytes := make(
|
|
[]byte, base64.StdEncoding.EncodedLen(len(c.x509Cert.Raw)))
|
|
base64.StdEncoding.Encode(xcertBytes, c.x509Cert.Raw)
|
|
err := pem.Encode(&builder, &pem.Block{Type: "CERTIFICATE", Bytes: xcertBytes})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to pem encode the X certificate: %w", err)
|
|
}
|
|
// Next write the private key
|
|
privBytes, err := x509.MarshalPKCS8PrivateKey(c.privateKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
err = pem.Encode(&builder, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to encode private key: %w", err)
|
|
}
|
|
|
|
return builder.String(), nil
|
|
}
|